From 9ff54bb6ecc7bb74e8fba4fc4184283f944fe7d6 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Thu, 29 Jan 2026 19:38:51 -0600 Subject: [PATCH 01/10] Added Application Information Card to Application Settings. Added User Guide and Database Management Guide to repository documentation. --- .gitignore | 3 + .../Constants/ApplicationSettings.cs | 8 +- .../Entities/OrganizationSettings.cs | 2 +- .../Services/ApplicationService.cs | 7 + .../OrganizationUsers/UserAccessCard.razor | 2 +- .../OrganizationUsers/UserProfileCard.razor | 3 + .../OrganizationUserStatistics.razor | 2 +- .../Organizations/OrganizationDetails.razor | 5 +- .../Settings/Pages/ApplicationSettings.razor | 79 +- 4-Aquiis.SimpleStart/appsettings.json | 8 +- Documentation/Database-Management-Guide.md | 1099 +++++++++++++++++ Documentation/Quick-Start-Guide.md | 757 ++++++++++++ LICENSE | 21 + 13 files changed, 1976 insertions(+), 20 deletions(-) create mode 100644 Documentation/Database-Management-Guide.md create mode 100644 Documentation/Quick-Start-Guide.md create mode 100644 LICENSE diff --git a/.gitignore b/.gitignore index c4e69aa..abffd24 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ publish_profile.xml .venv/ obj/ + +# VS Code user-specific settings (keep launch.json and tasks.json) +.vscode/settings.json diff --git a/0-Aquiis.Core/Constants/ApplicationSettings.cs b/0-Aquiis.Core/Constants/ApplicationSettings.cs index 80fc7e8..c1b4dfc 100644 --- a/0-Aquiis.Core/Constants/ApplicationSettings.cs +++ b/0-Aquiis.Core/Constants/ApplicationSettings.cs @@ -3,13 +3,19 @@ namespace Aquiis.Core.Constants public class ApplicationSettings { public string AppName { get; set; } = string.Empty; + public string ProductName { 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 string DatabaseFileName { get; set; } = string.Empty; + public string PreviousDatabaseFileName { get; set; } = string.Empty; public bool SoftDeleteEnabled { get; set; } - public string SchemaVersion { get; set; } = "1.0.0"; + public string SchemaVersion { get; set; } = string.Empty; public int MaxOrganizationUsers { get; set; } = 0; // 0 = unlimited (Professional), 3 = SimpleStart limit + public string License { get; set; } = string.Empty; + public string LicenseUrl { get; set; } = string.Empty; + public string HelpUrl { get; set; } = string.Empty; } // Property & Tenant Lifecycle Enums diff --git a/0-Aquiis.Core/Entities/OrganizationSettings.cs b/0-Aquiis.Core/Entities/OrganizationSettings.cs index fed6227..94f2945 100644 --- a/0-Aquiis.Core/Entities/OrganizationSettings.cs +++ b/0-Aquiis.Core/Entities/OrganizationSettings.cs @@ -30,7 +30,7 @@ public class OrganizationSettings : BaseModel [Required] [Range(0, 30)] [Display(Name = "Grace Period (Days)")] - public int LateFeeGracePeriodDays { get; set; } = 3; + public int LateFeeGracePeriodDays { get; set; } = 5; [Required] [Range(0, 1)] diff --git a/2-Aquiis.Application/Services/ApplicationService.cs b/2-Aquiis.Application/Services/ApplicationService.cs index 4182b35..14ce0ec 100644 --- a/2-Aquiis.Application/Services/ApplicationService.cs +++ b/2-Aquiis.Application/Services/ApplicationService.cs @@ -9,6 +9,8 @@ public class ApplicationService private readonly ApplicationSettings _settings; private readonly PaymentService _paymentService; private readonly LeaseService _leaseService; + + public ApplicationSettings ApplicationSettings => _settings; public bool SoftDeleteEnabled { get; } @@ -28,6 +30,11 @@ public string GetAppInfo() return $"{_settings.AppName} - {_settings.Version}"; } + public string GetDatabaseInfo() + { + return $"{_settings.DatabaseFileName} - v{_settings.SchemaVersion}"; + } + /// /// Gets the total payments received for a specific date /// diff --git a/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserAccessCard.razor b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserAccessCard.razor index 412d251..75d0f22 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserAccessCard.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserAccessCard.razor @@ -4,7 +4,7 @@ @namespace Aquiis.UI.Shared.Components.Entities.OrganizationUsers -
+
Your Access
diff --git a/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserProfileCard.razor b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserProfileCard.razor index a19e4d4..29b0746 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserProfileCard.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserProfileCard.razor @@ -2,6 +2,9 @@ @namespace Aquiis.UI.Shared.Components.Entities.OrganizationUsers
+
+
User Profile
+
-
+
Quick Stats
diff --git a/3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationDetails.razor b/3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationDetails.razor index ae24f76..929f411 100644 --- a/3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationDetails.razor +++ b/3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationDetails.razor @@ -22,15 +22,14 @@
- - - @if (CurrentUserViewModel != null) { } + +
diff --git a/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/ApplicationSettings.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/ApplicationSettings.razor index cabdb35..530b4c0 100644 --- a/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/ApplicationSettings.razor +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/ApplicationSettings.razor @@ -20,6 +20,8 @@ @inject NavigationManager Navigation @inject ApplicationDbContext DbContext @inject CalendarSettingsService CalendarSettingsService + +@inject ApplicationService ApplicationService @inject ILogger Logger @attribute [OrganizationAuthorize("Owner", "Administrator")] @@ -280,7 +282,7 @@
- + @*
Settings Summary
@@ -298,42 +300,97 @@ }
+
*@ + + +
+
+
Application Information
+
+
+
+
Product Name
+
@ApplicationService.ApplicationSettings.ProductName
+ +
Version
+
@ApplicationService.ApplicationSettings.Version
+ +
Database Version
+
@ApplicationService.ApplicationSettings.SchemaVersion
+ +
License
+
+ MIT License + + View + +
+ +
Repository
+
+ + GitHub + +
+
+
-
+
About Settings
-
Late Fees
+
Late Fees

Settings apply to all properties and tenants. Changes take effect immediately but do not retroactively affect existing invoices.

-
Background Tasks
+
Background Tasks

- Tasks run automatically daily at 2:00 AM. Use manual execution for testing or immediate processing. + Tasks run automatically at application start-up. Click "Run Now" for immediate processing.

-
Calendar
+
Calendar

Control which event types appear on your calendar. You can always add custom events manually for any category.

-
+ +
-
Tips
+
Documentation
-
- - SimpleStart Focus: These settings provide essential property management functionality. +
+ +
diff --git a/4-Aquiis.SimpleStart/appsettings.json b/4-Aquiis.SimpleStart/appsettings.json index acbbea3..e710202 100644 --- a/4-Aquiis.SimpleStart/appsettings.json +++ b/4-Aquiis.SimpleStart/appsettings.json @@ -13,15 +13,19 @@ "AllowedHosts": "*", "ApplicationSettings": { "AppName": "Aquiis", + "ProductName": "Aquiis SimpleStart", "Version": "1.0.1", "Author": "CIS Guru", "Email": "cisguru@outlook.com", "Repository": "https://github.com/xnodeoncode/Aquiis", "SoftDeleteEnabled": true, "DatabaseFileName": "app_v1.0.0.db", - "PreviousDatabaseFileName": "", + "PreviousDatabaseFileName": "app_v0.3.0.db", "SchemaVersion": "1.0.0", - "MaxOrganizationUsers": 3 + "MaxOrganizationUsers": 3, + "License": "MIT", + "LicenseUrl": "https://github.com/xnodeoncode/Aquiis/blob/main/LICENSE", + "HelpUrl": "https://github.com/xnodeoncode/Aquiis" }, "SessionTimeout": { "InactivityTimeoutMinutes": 18, diff --git a/Documentation/Database-Management-Guide.md b/Documentation/Database-Management-Guide.md new file mode 100644 index 0000000..effcb83 --- /dev/null +++ b/Documentation/Database-Management-Guide.md @@ -0,0 +1,1099 @@ +# Aquiis SimpleStart - Database Management Guide + +**Version:** 1.0.0 +**Last Updated:** January 28, 2026 +**Audience:** Administrators and Power Users + +--- + +## 📖 Table of Contents + +1. [Overview](#overview) +2. [Database Location & Structure](#database-location--structure) +3. [Backup Procedures](#backup-procedures) +4. [Restore Procedures](#restore-procedures) +5. [Database Reset](#database-reset) +6. [Database Health Checks](#database-health-checks) +7. [Troubleshooting](#troubleshooting) +8. [Advanced Topics](#advanced-topics) +9. [Best Practices](#best-practices) +10. [FAQ](#faq) + +--- + +## 📋 Overview + +Aquiis SimpleStart uses **SQLite** as its database engine - a lightweight, file-based database that requires no server installation or configuration. This guide covers everything you need to know about managing your database, protecting your data, and recovering from issues. + +### What You'll Learn + +- ✅ How to back up your database (manual and automatic) +- ✅ How to restore from a backup +- ✅ How to check database health +- ✅ How to troubleshoot common database issues +- ✅ How to optimize database performance +- ✅ Best practices for data protection + +### Why Database Management Matters + +Your database contains **all your property management data:** + +- Properties and tenants +- Leases and financial records +- Invoices and payments +- Maintenance requests and inspections +- Documents and photos +- User accounts and settings + +**Regular backups** are your insurance policy against: + +- Hardware failure (hard drive crash, SSD failure) +- Software bugs or corruption +- Accidental data deletion +- Ransomware or malware +- Natural disasters or theft + +--- + +## 💾 Database Location & Structure + +### Database File Location + +**Linux:** + +```bash +~/.config/Aquiis/Data/app_v1.0.0.db +``` + +**Windows:** + +``` +%APPDATA%\Aquiis\Data\app_v1.0.0.db +``` + +**Full path examples:** + +- **Linux:** `/home/username/.config/Aquiis/Data/app_v1.0.0.db` +- **Windows:** `C:\Users\YourName\AppData\Roaming\Aquiis\Data\app_v1.0.0.db` + +### Database Structure + +The database file is a **single SQLite file** containing: + +- All tables and indexes +- All data (properties, tenants, leases, etc.) +- Schema version information +- Configuration settings + +**File size:** Varies based on usage + +- New install: ~5 MB (with schema only) +- After 1 year: 10-50 MB (typical landlord) +- After 5 years: 50-200 MB (depends on photos/documents) + +### Schema Version + +Aquiis SimpleStart v1.0.0 uses schema version `1.0.0`. This tracks the database structure and ensures compatibility. + +**Check your schema version:** + +1. Navigate to **Settings** → **Database** +2. View **"Database Information"** panel +3. See **"Schema Version: 1.0.0"** + +### Auto-Migration System + +When you upgrade Aquiis SimpleStart to a new version: + +1. Application detects current schema version +2. Compares with required schema version +3. Automatically applies migrations if needed +4. Updates schema version in database +5. Application starts normally + +**No manual intervention required** - migrations are automatic and seamless! + +--- + +## 🔐 Backup Procedures + +### Why Back Up? + +**Your database should be backed up regularly because:** + +- Hardware can fail without warning +- Human error happens (accidental deletions) +- Software bugs can cause corruption +- You may need to revert to a previous state + +**How often to back up:** + +- **Before major changes** - Always (e.g., bulk imports, data migrations) +- **Daily** - Recommended for active property managers +- **Weekly** - Minimum for most users +- **Monthly** - Acceptable for very light usage + +### Method 1: Manual Backup (Recommended Before Major Changes) + +**Step-by-step:** + +1. **Open Aquiis SimpleStart** +2. **Navigate to:** Settings → Database +3. **Click:** "Backup & Restore" tab +4. **Click:** "Create Backup" button +5. **Wait:** Progress indicator shows backup creation +6. **Confirmation:** "Backup created successfully!" message appears + +**Backup details:** + +- **Filename:** `backup_YYYYMMDD_HHMMSS.db` + - Example: `backup_20260128_143022.db` (January 28, 2026 at 2:30:22 PM) +- **Location:** `Data/Backups/` folder + - Linux: `~/.config/Aquiis/Data/Backups/` + - Windows: `%APPDATA%\Aquiis\Data\Backups\` +- **Size:** Same as database file (~5-200 MB) + +**Best practice:** Create a manual backup **before**: + +- Upgrading to a new version +- Importing large amounts of data +- Making bulk changes (e.g., deleting multiple records) +- Testing new features + +### Method 2: Scheduled Automatic Backups (Recommended for Daily Use) + +Set up automatic backups that run on a schedule: + +**Configuration:** + +1. **Navigate to:** Settings → Database → Backup & Restore +2. **Enable:** "Schedule Automatic Backups" toggle (turn ON) +3. **Choose frequency:** + - Daily (recommended) + - Weekly + - Monthly +4. **Set backup time:** + - Default: 2:00 AM (when computer is idle) + - Choose a time when application is running +5. **Set retention policy:** + - Keep backups for: 7 days, 30 days, 90 days, Forever + - Older backups automatically deleted to save space +6. **Click:** "Save Settings" + +**Important notes:** + +- ⚠️ **Computer must be on** for scheduled backups to run +- ⚠️ **Application must be running** (can be minimized) +- ✅ Backups run in background (no interruption to your work) +- ✅ Failed backups logged and retried next scheduled time + +**Verify automatic backups are working:** + +1. Check the "Last Backup" timestamp in Database settings +2. Browse to `Data/Backups/` folder +3. Verify recent backup files exist +4. Check file dates match your schedule + +### Method 3: External Backup (Cloud or USB Drive) + +For **maximum protection**, copy backups to multiple locations: + +**Cloud storage examples:** + +- Google Drive +- Dropbox +- OneDrive +- iCloud Drive +- Backblaze +- AWS S3 + +**USB/External drive backup:** + +**Linux:** + +```bash +# Copy all backups to USB drive +cp ~/.config/Aquiis/Data/Backups/*.db /media/usb/AquiisBackups/ + +# Or use rsync for incremental backups +rsync -av ~/.config/Aquiis/Data/Backups/ /media/usb/AquiisBackups/ +``` + +**Windows:** + +```powershell +# Copy all backups to USB drive +Copy-Item "$env:APPDATA\Aquiis\Data\Backups\*.db" "E:\AquiisBackups\" -Recurse +``` + +**Automated cloud sync:** + +Many cloud services offer **folder sync** - configure your cloud client to sync the Backups folder: + +1. Install cloud storage client (e.g., Dropbox, Google Drive) +2. Configure sync for: `Data/Backups/` folder +3. Backups automatically uploaded to cloud +4. Access your backups from anywhere + +**Best practice:** Use the **3-2-1 backup rule**: + +- **3** copies of your data (original + 2 backups) +- **2** different storage types (local SSD + external USB) +- **1** off-site backup (cloud storage) + +--- + +## 🔄 Restore Procedures + +### When to Restore + +Restore from backup when: + +- Database corruption detected +- Accidental data deletion occurred +- Need to revert to previous state +- Migrating to new computer +- Testing or training scenarios + +### Method 1: Staged Restore (Preview Before Committing) + +**Staged restore** lets you preview a backup before fully restoring it: + +**Step-by-step:** + +1. **Navigate to:** Settings → Database → Backup & Restore +2. **Click:** "Available Backups" dropdown +3. **Select** a backup from the list: + - Shows filename and date + - Example: `backup_20260128_143022.db (Jan 28, 2026 2:30 PM)` +4. **Click:** "Staged Restore" button +5. **Wait:** System creates temporary copy and validates backup +6. **Preview:** Backup information shown: + - Backup date and time + - Database size + - Schema version + - Record counts (properties, tenants, leases) +7. **Decision point:** + - If backup looks correct: **Click "Confirm Restore"** + - If backup is wrong: **Click "Cancel"** (no changes made) + +**What happens during staged restore:** + +1. Backup file copied to staging area (`Data/Staging/`) +2. Backup validated (integrity check) +3. Schema version verified +4. Record counts displayed for review +5. Original database **NOT modified** until you confirm + +**Advantages:** + +- ✅ Safe - preview before committing +- ✅ Verify backup integrity before restore +- ✅ See what data will be restored +- ✅ Can cancel without risk + +### Method 2: Full Restore (Direct Restore with Restart) + +**Full restore** replaces your current database immediately: + +**Step-by-step:** + +1. **Navigate to:** Settings → Database → Backup & Restore +2. **Click:** "Available Backups" dropdown +3. **Select** a backup file +4. **Click:** "Restore from Backup" button +5. **Confirmation dialog** appears: + + ``` + ⚠️ Warning: This will replace your current database + + Current database will be backed up automatically before restore. + Application will restart after restore completes. + + Are you sure you want to continue? + ``` + +6. **Click:** "Yes, Restore" to proceed (or "Cancel" to abort) +7. **Automatic steps:** + - Current database backed up (safety backup) + - Selected backup validated + - Current database replaced with backup + - Application restarts automatically +8. **Verification:** After restart, check data to ensure correct + +**Safety features:** + +- ✅ Current database automatically backed up before restore +- ✅ Restore can be undone (restore the safety backup) +- ✅ Validation prevents corrupted backup from being used +- ✅ Application restart ensures clean state + +**Important notes:** + +- ⚠️ **All users logged out** during restore +- ⚠️ **Unsaved work will be lost** - save everything first +- ⚠️ **Takes 10-30 seconds** depending on database size + +### Method 3: Manual Restore (Advanced Users) + +If the application won't start or database UI is unavailable: + +**Linux:** + +```bash +# Stop the application first +pkill Aquiis + +# Navigate to data directory +cd ~/.config/Aquiis/Data/ + +# Backup current (corrupted) database +mv app_v1.0.0.db app_v1.0.0.db.corrupted + +# Copy backup to main database +cp Backups/backup_20260128_143022.db app_v1.0.0.db + +# Restart application +./Aquiis-SimpleStart-1.0.0.AppImage +``` + +**Windows:** + +```powershell +# Stop the application first (close window or Task Manager) + +# Navigate to data directory +cd $env:APPDATA\Aquiis\Data + +# Backup current (corrupted) database +Move-Item app_v1.0.0.db app_v1.0.0.db.corrupted + +# Copy backup to main database +Copy-Item Backups\backup_20260128_143022.db app_v1.0.0.db + +# Restart application (Start Menu or double-click icon) +``` + +**Verify restore was successful:** + +1. Application starts without errors +2. Login works normally +3. Data appears correct (check critical records) +4. No error messages in logs + +--- + +## 🔄 Database Reset + +### What is Database Reset? + +**Database reset** removes all data and returns the application to **first-run state**: + +- All properties, tenants, leases **deleted** +- All financial records **deleted** +- All documents and photos **deleted** +- Organization and user accounts **deleted** +- You will go through New Setup Wizard again + +### When to Reset + +Reset the database when: + +- Starting fresh with new data +- Training or demonstration scenarios +- Selling/transferring application to someone else +- Troubleshooting requires clean slate +- Development/testing purposes + +**⚠️ Warning: Reset is PERMANENT and cannot be undone!** + +### Reset Procedure + +**Step-by-step:** + +1. **Navigate to:** Settings → Database → Backup & Restore +2. **Click:** "Advanced" tab +3. **Click:** "Reset Database" button (red button at bottom) +4. **Confirmation dialog** appears: + + ``` + ⚠️ DANGER: Reset Database + + This will DELETE ALL DATA including: + - All properties, tenants, and leases + - All invoices, payments, and financial records + - All maintenance requests and inspections + - All documents and photos + - Organization and user accounts + + A backup will be created automatically before reset. + + Type "RESET" to confirm (case-sensitive): + _______ + ``` + +5. **Type:** `RESET` (exactly, uppercase) +6. **Click:** "Confirm Reset" +7. **Automatic steps:** + - Current database backed up (safety backup: `pre-reset-backup_DATE.db`) + - Database reset to empty schema + - Application restarts to New Setup Wizard +8. **Setup:** Go through New Setup Wizard to configure fresh installation + +**Safety features:** + +- ✅ Automatic backup created before reset +- ✅ Typed confirmation required (prevents accidental clicks) +- ✅ Can restore from pre-reset backup if needed + +**Recovery from reset:** + +If you reset by mistake, **immediately restore** from the pre-reset backup: + +1. Navigate to Backups folder +2. Find: `pre-reset-backup_20260128_143022.db` +3. Use restore procedure (Method 2 or 3 above) +4. Your data will be recovered + +--- + +## 🏥 Database Health Checks + +### Automatic Health Monitoring + +Aquiis SimpleStart monitors database health continuously: + +- **On startup** - Integrity check before application loads +- **Every hour** - Background health check via scheduled tasks +- **Before backup** - Verify database is healthy before backup +- **After errors** - Check health if database errors occur + +### Manual Health Check + +**Run a manual health check:** + +1. **Navigate to:** Settings → Database → Health & Monitoring +2. **Click:** "Run Health Check" button +3. **Wait:** Health check runs (5-15 seconds) +4. **Results displayed:** + + ``` + ✅ Database Health Check: PASSED + + Database file: app_v1.0.0.db + File size: 47.3 MB + Schema version: 1.0.0 + Connection status: Connected + Integrity check: PASSED + + Record counts: + - Properties: 9 + - Tenants: 12 + - Leases: 8 + - Invoices: 96 + - Payments: 89 + + Last backup: January 28, 2026 2:00 AM + Next scheduled backup: January 29, 2026 2:00 AM + ``` + +### Health Indicators + +**Green indicators (healthy):** + +- ✅ **Connection status: Connected** - Database accessible +- ✅ **Integrity check: PASSED** - No corruption detected +- ✅ **Schema version: 1.0.0** - Matches application version +- ✅ **File accessible: Yes** - Database file readable and writable + +**Yellow indicators (warning):** + +- ⚠️ **File size large** - Database over 500 MB (consider cleanup) +- ⚠️ **No recent backup** - Last backup over 7 days ago +- ⚠️ **Slow queries** - Database performance degraded + +**Red indicators (critical):** + +- ❌ **Connection failed** - Cannot connect to database +- ❌ **Integrity check: FAILED** - Corruption detected +- ❌ **Schema mismatch** - Version incompatibility +- ❌ **File locked** - Another process using database + +### Database Optimization + +**When to optimize:** + +- Database feels slow +- Search queries take long time +- Application startup is slow +- After bulk data operations (imports, deletions) + +**Run optimization:** + +1. **Navigate to:** Settings → Database → Health & Monitoring +2. **Click:** "Optimize Database" button +3. **Wait:** Optimization runs (30 seconds to 2 minutes) +4. **Automatic steps:** + - VACUUM command (reclaim unused space) + - ANALYZE command (update statistics) + - Index rebuilding + - Cache refresh +5. **Results:** Performance improvements and size reduction + +**Expected results:** + +- **Speed:** Queries 20-50% faster +- **Size:** Database file 10-30% smaller +- **Startup:** Application launches faster + +**Recommendation:** Optimize database quarterly (every 3 months) + +--- + +## 🔧 Troubleshooting + +### Issue 1: Database Locked + +**Symptoms:** + +- Error: "Database is locked" +- Cannot save changes +- Application hangs on database operations + +**Causes:** + +- Another Aquiis instance running +- Background backup in progress +- Database file open in SQLite browser tool +- System file lock from crash + +**Solutions:** + +1. **Check for multiple instances:** + + ```bash + # Linux + ps aux | grep Aquiis + + # Windows (Task Manager) + # Look for multiple Aquiis.SimpleStart.exe processes + ``` + + - Close extra instances + - Restart application + +2. **Wait for backup to complete:** + - Check if backup is running (Settings → Database) + - Wait 1-2 minutes for backup to finish + +3. **Close database tools:** + - Close SQLite browser, DB Browser for SQLite, etc. + - These lock the database file + +4. **Reboot computer:** + - Last resort if file locks persist + - Clears all locks and processes + +5. **Manual lock file removal (advanced):** + + ```bash + # Only if application is definitely closed + # Linux + rm ~/.config/Aquiis/Data/app_v1.0.0.db-wal + rm ~/.config/Aquiis/Data/app_v1.0.0.db-shm + + # Windows + del %APPDATA%\Aquiis\Data\app_v1.0.0.db-wal + del %APPDATA%\Aquiis\Data\app_v1.0.0.db-shm + ``` + +### Issue 2: Database Corruption + +**Symptoms:** + +- Error: "Database disk image is malformed" +- Application crashes on startup +- Random data disappears +- Integrity check fails + +**Causes:** + +- Hard drive failure or bad sectors +- System crash during write operation +- Power loss during database update +- SQLite bug (rare) + +**Solutions:** + +1. **Restore from backup (recommended):** + - Use most recent backup + - Follow restore procedure above + - Verify data after restore + +2. **SQLite recovery tool (if no backup):** + + ```bash + # Attempt to dump and rebuild database + sqlite3 app_v1.0.0.db ".dump" | sqlite3 app_v1.0.0_recovered.db + + # Replace corrupted database with recovered one + mv app_v1.0.0.db app_v1.0.0.db.corrupted + mv app_v1.0.0_recovered.db app_v1.0.0.db + ``` + +3. **Professional data recovery:** + - If data is critical and no backup exists + - Contact data recovery specialist + - May be expensive ($500-$2000) + +**Prevention:** + +- ✅ Enable automatic daily backups +- ✅ Store backups off-site (cloud or USB) +- ✅ Use UPS (uninterruptible power supply) +- ✅ Monitor hard drive health (SMART status) + +### Issue 3: Migration Failures + +**Symptoms:** + +- Application won't start after update +- Error: "Migration failed" +- Schema version mismatch + +**Causes:** + +- Interrupted migration (crash during update) +- Corrupted migration files +- Schema version conflict + +**Solutions:** + +1. **Check logs:** + - Navigate to: `Data/Logs/` + - Open most recent log file + - Search for "migration" or "schema" + - Error details help diagnose issue + +2. **Rollback to previous version:** + - Uninstall current version + - Install previous version + - Restore backup from before update + - Contact support for migration help + +3. **Manual schema repair (advanced):** + - Requires SQLite knowledge + - Export data, fix schema, reimport + - Not recommended for non-technical users + +4. **Fresh install with data export:** + - Export critical data manually (or via backup) + - Reset database + - Reinstall application + - Manually re-enter data + +**Prevention:** + +- ✅ **Always backup before updating** +- ✅ Test updates on non-production copy first +- ✅ Wait 1-2 weeks after release before updating (let others test) + +### Issue 4: Slow Database Performance + +**Symptoms:** + +- Slow search results +- Long application startup +- Lagging UI when navigating +- High CPU usage + +**Causes:** + +- Large database file (>500 MB) +- Many soft-deleted records +- Missing or outdated indexes +- Insufficient RAM + +**Solutions:** + +1. **Optimize database:** + - Settings → Database → Optimize + - Rebuilds indexes and reclaims space + +2. **Cleanup old data:** + - Permanently delete soft-deleted records (advanced feature) + - Archive old financial records + - Remove unused documents/photos + +3. **Check system resources:** + - Close other applications + - Increase RAM if below minimum (2 GB) + - Use SSD instead of HDD for data folder + +4. **Split database (advanced):** + - Not officially supported in v1.0.0 + - Contact support for guidance if needed + +### Issue 5: Cannot Find Database File + +**Symptoms:** + +- Error: "Database file not found" +- Application shows empty data +- Fresh installation wizard appears + +**Causes:** + +- Database file moved or deleted +- Incorrect data folder location +- Permissions issue (Linux) + +**Solutions:** + +1. **Check default locations:** + + ```bash + # Linux + ls -la ~/.config/Aquiis/Data/ + + # Windows + dir %APPDATA%\Aquiis\Data + ``` + +2. **Search for database file:** + + ```bash + # Linux + find ~ -name "app_v1.0.0.db" 2>/dev/null + + # Windows (PowerShell) + Get-ChildItem -Path C:\ -Filter "app_v1.0.0.db" -Recurse -ErrorAction SilentlyContinue + ``` + +3. **Restore from backup:** + - If original database lost + - Copy backup to correct location + - Rename to `app_v1.0.0.db` + +4. **Fix permissions (Linux):** + ```bash + chmod 644 ~/.config/Aquiis/Data/app_v1.0.0.db + chown $USER:$USER ~/.config/Aquiis/Data/app_v1.0.0.db + ``` + +--- + +## 🎓 Advanced Topics + +### Database File Format + +**SQLite version:** 3.x +**Encoding:** UTF-8 +**Page size:** 4096 bytes +**Journal mode:** WAL (Write-Ahead Logging) + +### WAL Files + +You may notice these files alongside the database: + +- `app_v1.0.0.db` - Main database file +- `app_v1.0.0.db-wal` - Write-Ahead Log (temporary) +- `app_v1.0.0.db-shm` - Shared Memory file (temporary) + +**WAL benefits:** + +- Better concurrency (reads don't block writes) +- Faster writes +- Atomic commits + +**Important:** When backing up, include **only** `.db` file (not WAL/SHM). Application will recreate WAL files automatically. + +### Direct Database Access (Advanced Users) + +You can query the database directly using SQLite tools: + +**Tools:** + +- SQLite command-line: `sqlite3` +- DB Browser for SQLite (GUI) +- SQLiteStudio +- DBeaver + +**Example queries:** + +```sql +-- View all properties +SELECT * FROM Properties WHERE IsDeleted = 0; + +-- Count active leases +SELECT COUNT(*) FROM Leases WHERE Status = 'Active'; + +-- Total rent collected this month +SELECT SUM(Amount) FROM Payments WHERE PaymentDate >= '2026-01-01'; + +-- Find overdue invoices +SELECT * FROM Invoices WHERE DueDate < date('now') AND Status = 'Pending'; +``` + +**⚠️ Warning:** + +- **Read-only** recommended (SELECT queries only) +- **Do NOT** modify data directly (UPDATE, DELETE, INSERT) +- Bypasses application logic and audit trails +- Can cause corruption or data inconsistencies +- Close tool before starting Aquiis (prevents lock conflicts) + +### Data Export + +Export data for analysis or migration: + +```sql +-- Export properties to CSV +.mode csv +.output properties.csv +SELECT * FROM Properties WHERE IsDeleted = 0; +.output stdout + +-- Export financial summary +.output financial_summary.csv +SELECT + l.PropertyId, + p.PropertyName, + COUNT(DISTINCT i.Id) as InvoiceCount, + SUM(i.Amount) as TotalBilled, + SUM(i.AmountPaid) as TotalPaid +FROM Leases l +JOIN Properties p ON l.PropertyId = p.Id +LEFT JOIN Invoices i ON i.LeaseId = l.Id +WHERE l.IsDeleted = 0 +GROUP BY l.PropertyId, p.PropertyName; +.output stdout +``` + +### Database Encryption + +SQLite databases are **not encrypted** by default. To add encryption: + +**Option 1: SQLCipher (not included in v1.0.0)** + +SQLCipher provides AES-256 encryption. Would require custom build. + +**Option 2: File-system encryption** + +- **Linux:** LUKS encrypted partition or eCryptfs home folder +- **Windows:** BitLocker drive encryption +- **macOS:** FileVault + +**Option 3: Third-party encryption tools** + +- VeraCrypt (cross-platform encrypted volumes) +- 7-Zip (password-protected archives) + +**Recommendation:** Use full-disk encryption (BitLocker, FileVault) for simplicity. + +### Multi-Computer Sync + +Aquiis SimpleStart is designed as single-computer desktop application. Multi-computer sync is **not officially supported** but possible with caveats: + +**Cloud sync approach:** + +1. Place database file in synced folder (Dropbox, Google Drive) +2. Configure symlink to synced location +3. Only use on **one computer at a time** (avoid conflicts) + +**⚠️ Risks:** + +- Database corruption if both computers write simultaneously +- Sync conflicts if edits made offline +- Poor performance over network + +**Recommendation:** Use one computer as primary. For multi-computer access, wait for Aquiis Professional (web-based, v2.0.0). + +--- + +## 📌 Best Practices + +### Daily Operations + +1. **Enable automatic backups** + - Set to daily at 2 AM + - Verify backups run successfully (check Last Backup timestamp) + +2. **Monitor database health** + - Check health status weekly + - Review any warnings/errors + +3. **Save work regularly** + - Application auto-saves most changes + - Exit normally (don't force quit) + +### Weekly Maintenance + +1. **Verify backups** + - Check `Data/Backups/` folder + - Verify recent backup files exist + - Spot-check a backup (staged restore preview) + +2. **Review disk space** + - Ensure adequate free space (500 MB minimum) + - Clean up if low on space + +### Monthly Maintenance + +1. **Copy backups off-site** + - Upload to cloud storage + - Copy to external USB drive + - Test restore from off-site backup + +2. **Review retention policy** + - Delete very old backups if needed + - Keep at least 3 months of backups + +### Quarterly Maintenance + +1. **Optimize database** + - Settings → Database → Optimize + - Improves performance + +2. **Test restore procedure** + - Practice restoring from backup + - Verify you can recover data if needed + +3. **Archive old data (optional)** + - Export old financial records + - Permanently delete soft-deleted records (if feature available) + +### Before Major Updates + +1. **Create manual backup** + - Settings → Database → Create Backup + +2. **Copy backup to safe location** + - USB drive or cloud storage + +3. **Document current state** + - Note schema version + - List critical data (property count, active leases) + +4. **Test update on copy (if possible)** + - Duplicate database + - Test update on copy first + +--- + +## ❓ FAQ + +### Q: How often should I back up my database? + +**A:** + +- **Minimum:** Weekly +- **Recommended:** Daily (automatic) +- **Best practice:** Daily automatic + off-site copy weekly + +### Q: Where are backups stored? + +**A:** + +- **Linux:** `~/.config/Aquiis/Data/Backups/` +- **Windows:** `%APPDATA%\Aquiis\Data\Backups\` + +### Q: Can I restore a backup from an older version? + +**A:** Generally yes, if schema versions are compatible. The application will attempt to migrate the backup forward. However, backups from much older versions (e.g., v0.1.0 → v1.0.0) may not work. Always test staged restore first. + +### Q: What if I lose all my backups? + +**A:** If both the main database and all backups are lost, **data cannot be recovered** unless you have off-site backups (cloud, USB drive). This is why the 3-2-1 backup rule is critical. + +### Q: Can I access the database while Aquiis is running? + +**A:** For **read-only** queries with SQLite tools: YES (WAL mode allows concurrent reads). For **write operations**: NO (will cause database lock conflicts). + +### Q: How do I migrate to a new computer? + +**A:** + +1. On old computer: Create backup +2. Copy backup file to new computer (USB drive, cloud) +3. Install Aquiis SimpleStart on new computer +4. Copy backup to: `Data/Backups/` folder on new computer +5. Restore from backup + +Or simply copy the entire `Data/` folder to new computer. + +### Q: Can I use my database on both Windows and Linux? + +**A:** YES! SQLite databases are cross-platform. Copy the `.db` file between systems freely. + +### Q: What happens if my hard drive fails? + +**A:** If you have **off-site backups** (cloud, USB), you can recover your data. If not, data is **permanently lost** (professional data recovery may be possible but expensive and not guaranteed). + +### Q: Should I backup the WAL files too? + +**A:** NO. Only backup the main `.db` file. WAL and SHM files are temporary and recreated automatically. + +### Q: Can I edit the database directly instead of using the application? + +**A:** **Not recommended.** Direct edits bypass: + +- Validation rules +- Audit trails (CreatedBy, LastModifiedOn) +- Business logic (e.g., property status updates) +- Relationships (foreign keys may break) + +Use application UI for all changes. + +### Q: How large can the database get? + +**A:** SQLite theoretical limit is **281 TB**. Practical limit for Aquiis SimpleStart: **2-4 GB** (depends on available RAM). A typical small landlord database: **50-200 MB** after 5 years. + +### Q: Can I have multiple databases? + +**A:** Not directly supported in v1.0.0. You can manually swap database files, but only one can be active at a time. For multi-organization use, wait for Aquiis Professional. + +--- + +## 📞 Support + +Need help with database management? We're here for you! + +**Email:** cisguru@outlook.com +**GitHub Issues:** [https://github.com/xnodeoncode/Aquiis/issues](https://github.com/xnodeoncode/Aquiis/issues) + +**When contacting support about database issues, include:** + +1. **Exact error message** (copy full text) +2. **Steps to reproduce** (what you were doing when error occurred) +3. **Database size** (from Settings → Database) +4. **Last successful backup** (date/time) +5. **Recent changes** (upgrades, bulk operations, etc.) +6. **Log files** (Settings → System → Export Logs) + +--- + +## 🎓 Summary + +**Key takeaways:** + +✅ **Enable automatic daily backups** - Your insurance policy +✅ **Store backups off-site** - Cloud or USB drive +✅ **Test restore occasionally** - Practice before you need it +✅ **Monitor database health** - Catch issues early +✅ **Optimize quarterly** - Maintain performance +✅ **Backup before major changes** - Always + +**Follow the 3-2-1 backup rule:** + +- **3** copies of data (original + 2 backups) +- **2** different storage types (local + external) +- **1** off-site backup (cloud or remote location) + +**With good database management practices, your data is safe and your business runs smoothly!** 🏠 + +--- + +**Document Version:** 1.0 +**Last Updated:** January 28, 2026 +**Author:** CIS Guru with GitHub Copilot diff --git a/Documentation/Quick-Start-Guide.md b/Documentation/Quick-Start-Guide.md new file mode 100644 index 0000000..4dc79a0 --- /dev/null +++ b/Documentation/Quick-Start-Guide.md @@ -0,0 +1,757 @@ +# Aquiis SimpleStart - Quick Start Guide + +**Version:** 1.0.0 +**Last Updated:** January 28, 2026 +**Estimated Time:** 15 minutes + +--- + +## 📖 Welcome! + +This guide will help you get started with Aquiis SimpleStart in just 15 minutes. By the end, you'll have: + +- ✅ Installed the application +- ✅ Created your organization +- ✅ Added your first property +- ✅ Added a tenant +- ✅ Created a lease +- ✅ Generated an invoice +- ✅ Recorded a payment +- ✅ Scheduled an inspection + +Let's get started! + +--- + +## 📋 Prerequisites + +Before you begin, ensure you have: + +- **Operating System:** + - Linux (Ubuntu 20.04+, Debian 11+, Fedora 35+), OR + - Windows 10/11 (64-bit) +- **Hardware:** + - 2 GB RAM minimum (4 GB recommended) + - 500 MB disk space +- **Downloaded:** Aquiis SimpleStart v1.0.0 installer for your platform + +--- + +## 🚀 Step 1: Installation (5 minutes) + +### Linux Installation + +**Option A: AppImage (Recommended for most users)** + +```bash +# 1. Download the file +# File: Aquiis-SimpleStart-1.0.0.AppImage + +# 2. Make it executable +chmod +x Aquiis-SimpleStart-1.0.0.AppImage + +# 3. Run the application +./Aquiis-SimpleStart-1.0.0.AppImage +``` + +**Option B: Debian Package (Ubuntu/Debian users)** + +```bash +# 1. Install the package +sudo dpkg -i Aquiis-SimpleStart-1.0.0-amd64.deb + +# 2. Run the application +aquiis-simplestart +``` + +### Windows Installation + +**Option A: NSIS Installer (Recommended)** + +1. **Download** `Aquiis-SimpleStart-Setup-1.0.0.exe` +2. **Double-click** the installer +3. **Follow the wizard:** + - Click "Next" to begin + - Accept license agreement + - Choose installation directory (default: `C:\Program Files\Aquiis SimpleStart\`) + - Create desktop shortcut (recommended) + - Click "Install" +4. **Launch** from Start Menu or Desktop shortcut + +**Option B: Portable Executable (No installation)** + +1. **Download** `Aquiis-SimpleStart-1.0.0-Portable.exe` +2. **Place** in your desired folder (e.g., `C:\Aquiis\`) +3. **Double-click** to run + +**✅ Checkpoint:** Application window should open showing the New Setup Wizard. + +--- + +## 🏢 Step 2: Create Your Organization (2 minutes) + +When you first launch Aquiis SimpleStart, the **New Setup Wizard** guides you through initial setup. + +### Organization Setup + +**On the "Create Organization" screen:** + +1. **Organization Name:** Enter your business name + - Example: "ABC Property Management" or "John Smith Rentals" +2. **Contact Information:** + - **Phone:** Your business phone number + - **Email:** Your business email address + - **Website:** (Optional) Your website URL + +3. **Address:** + - Street address + - City, State, ZIP code + - Country + +4. **Click** "Create Organization" + +**✅ Checkpoint:** You should see "Organization created successfully!" message. + +--- + +## 👤 Step 3: Register Your User Account (2 minutes) + +**On the "Register User" screen:** + +1. **Personal Information:** + - **First Name:** Your first name + - **Last Name:** Your last name + +2. **Login Credentials:** + - **Email:** Your email address (becomes your username) + - **Password:** Choose a strong password + - Minimum 8 characters + - Must contain: uppercase, lowercase, number, special character + - **Confirm Password:** Re-enter your password + +3. **Role:** Administrator (automatically selected for first user) + +4. **Click** "Register" + +**✅ Checkpoint:** You should be logged in and see the Dashboard. + +### First-Time Dashboard + +After registration, you'll see the main dashboard with: + +- **Navigation menu** on the left +- **Dashboard widgets** showing 0 properties, tenants, leases +- **Welcome message** with quick actions + +**Note:** Your account is automatically confirmed in SimpleStart (no email verification step). + +--- + +## 🏠 Step 4: Add Your First Property (3 minutes) + +Let's add a rental property to your portfolio. + +### Navigate to Properties + +1. Click **"Property Management"** in the left navigation menu +2. Click **"Properties"** +3. Click **"Add Property"** button (top-right) + +### Enter Property Details + +**Basic Information:** + +- **Property Name:** Give it a friendly name + - Example: "123 Main Street House" +- **Address:** + - **Street Address:** 123 Main Street + - **City:** Anytown + - **State/Province:** CA + - **ZIP/Postal Code:** 12345 + - **Country:** USA + +- **Property Type:** Select from dropdown + - Options: Single Family, Multi-Family, Apartment, Condo, Townhouse + - Choose: **Single Family** + +- **Number of Units:** 1 (for single-family home) + +**Financial Information:** + +- **Monthly Rent:** $1,500.00 +- **Security Deposit:** $1,500.00 (typically equal to one month's rent) + +**Property Status:** + +- Select: **Available** (ready to rent) + +**Description (Optional):** + +- Enter a brief description of the property +- Example: "Beautiful 3-bedroom, 2-bathroom single-family home with large backyard. Recently renovated kitchen with modern appliances." + +### Save Property + +1. Click **"Save"** button at the bottom +2. You'll be redirected to the property list + +**✅ Checkpoint:** You should see your property in the list with status "Available". + +### Add Property Photo (Optional) + +1. Click on your property name to view details +2. Click **"Upload Photo"** button +3. Select an image file (max 10MB) +4. Photo appears in property profile + +--- + +## 👥 Step 5: Add a Tenant (2 minutes) + +Now let's add a tenant who will rent this property. + +### Navigate to Tenants + +1. Click **"Tenant Management"** in left navigation +2. Click **"Tenants"** +3. Click **"Add Tenant"** button + +### Enter Tenant Details + +**Personal Information:** + +- **First Name:** Jane +- **Last Name:** Doe +- **Email:** jane.doe@example.com +- **Phone:** (555) 123-4567 + +**Current Address (Optional but Recommended):** + +- Street Address: 456 Oak Avenue +- City: Anytown +- State: CA +- ZIP: 12345 + +**Emergency Contact (Optional):** + +- Name: John Doe (Spouse) +- Relationship: Spouse +- Phone: (555) 123-4568 + +### Save Tenant + +1. Click **"Save"** button +2. You'll be redirected to the tenant list + +**✅ Checkpoint:** You should see Jane Doe in the tenant list. + +**Note:** In a real-world scenario, you'd go through the full prospect-to-tenant journey (application, screening, approval). For this Quick Start, we're creating the tenant directly. + +--- + +## 📄 Step 6: Create a Lease (2 minutes) + +Let's create a lease agreement between your property and tenant. + +### Navigate to Leases + +1. Click **"Lease Management"** in left navigation +2. Click **"Leases"** +3. Click **"Create Lease"** button + +### Enter Lease Details + +**Lease Information:** + +- **Property:** Select "123 Main Street House" from dropdown +- **Tenant:** Select "Jane Doe" from dropdown + +**Lease Terms:** + +- **Start Date:** Choose today's date (or desired move-in date) + - Example: February 1, 2026 +- **End Date:** Choose one year from start date + - Example: January 31, 2027 +- **Monthly Rent:** $1,500.00 (pre-filled from property) +- **Security Deposit:** $1,500.00 (pre-filled from property) +- **Due Day:** 1 (rent due on 1st of each month) + +**Payment Terms:** + +- **Late Fee Grace Period:** 5 days (rent due 1st, late fee applied on 6th) +- **Late Fee Amount:** $50.00 or 5% of rent +- **Payment Methods Accepted:** Check all that apply + - ☑ Cash + - ☑ Check + - ☑ Credit Card + - ☑ ACH + - ☑ Online Portal + +### Generate Lease + +1. Click **"Generate Lease"** button +2. System creates the lease with status: **Active** +3. Property status automatically changes to: **Occupied** + +**✅ Checkpoint:** You should see the new lease in the lease list with status "Active". + +### View Lease PDF (Optional) + +1. Click on the lease to view details +2. Click **"Download Lease PDF"** button +3. PDF opens showing complete lease agreement with all terms + +--- + +## 💰 Step 7: Generate a Rent Invoice (1 minute) + +Let's create the first month's rent invoice. + +### Navigate to Invoices + +1. Click **"Financial Management"** in left navigation +2. Click **"Invoices"** +3. Click **"Create Invoice"** button + +### Enter Invoice Details + +**Invoice Information:** + +- **Lease:** Select "Jane Doe - 123 Main Street House" from dropdown +- **Invoice Type:** Rent (from dropdown) +- **Description:** "February 2026 Rent" + +**Financial Details:** + +- **Amount:** $1,500.00 (pre-filled from lease monthly rent) +- **Due Date:** February 1, 2026 +- **Issue Date:** Today's date (auto-filled) + +**Status:** + +- **Invoice Status:** Pending (default) + +### Save Invoice + +1. Click **"Save"** button +2. Invoice is created and added to the list + +**✅ Checkpoint:** You should see the invoice with status "Pending" and due date of February 1, 2026. + +**Note:** In production, rent invoices are automatically generated monthly by the background task system. For this Quick Start, we're creating one manually. + +--- + +## 🏦 Step 8: Record a Payment (1 minute) + +Now let's record that the tenant paid their rent. + +### Navigate to Payments + +1. Stay in **"Financial Management"** +2. Click **"Payments"** +3. Click **"Record Payment"** button + +### Enter Payment Details + +**Payment Information:** + +- **Invoice:** Select "February 2026 Rent - Jane Doe" from dropdown +- **Payment Method:** Check (or select what applies) + +**Financial Details:** + +- **Amount:** $1,500.00 (full rent payment) +- **Payment Date:** Today's date (or actual payment date) +- **Payment Reference:** Check #1234 (optional but recommended) + +**Notes (Optional):** + +- "Check received and deposited on [date]" + +### Save Payment + +1. Click **"Save"** button +2. Payment is recorded +3. Invoice status automatically updates to: **Paid** +4. Invoice "Amount Paid" field updates to $1,500.00 + +**✅ Checkpoint:** When you view the invoice list, the invoice should now show status "Paid" with green indicator. + +### View Payment Receipt (Optional) + +1. Click on the payment to view details +2. Click **"Generate Receipt"** button +3. PDF receipt generated showing payment confirmation + +--- + +## 🔍 Step 9: Schedule an Inspection (1 minute) + +Let's schedule a move-in inspection for the property. + +### Navigate to Inspections + +1. Click **"Maintenance & Inspections"** in left navigation +2. Click **"Inspections"** +3. Click **"Schedule Inspection"** button + +### Enter Inspection Details + +**Inspection Information:** + +- **Property:** Select "123 Main Street House" +- **Inspection Type:** Move-In (from dropdown) +- **Inspection Date:** Choose today's date or soon after move-in + +**Scheduled By:** + +- Your name (auto-filled) + +**Notes (Optional):** + +- "Initial move-in inspection before tenant occupancy" + +### Save Inspection + +1. Click **"Schedule"** button +2. Inspection is added to calendar and inspection list + +**✅ Checkpoint:** You should see the inspection scheduled on the calendar and in the inspection list. + +### Complete Inspection (Optional) + +Later, when you perform the inspection: + +1. Click on the inspection from the list +2. Click **"Start Inspection"** button +3. Go through **26-item checklist:** + - **Exterior** (4 items): Roof, Siding, Windows, Landscaping + - **Interior** (6 items): Walls, Floors, Ceilings, Doors, Closets, Light Fixtures + - **Kitchen** (4 items): Appliances, Cabinets, Countertops, Sink/Plumbing + - **Bathroom** (4 items): Fixtures, Toilet, Sink, Shower/Tub + - **Systems** (8 items): HVAC, Electrical, Plumbing, Water Heater, etc. +4. Mark each item: **Pass** / **Fail** / **Needs Repair** +5. Add notes for any issues found +6. Click **"Complete Inspection"** +7. Click **"Generate PDF Report"** + +--- + +## 🎉 Congratulations! + +**You've successfully completed the Quick Start Guide!** + +In just 15 minutes, you've learned how to: + +- ✅ Install Aquiis SimpleStart +- ✅ Create your organization +- ✅ Register your user account +- ✅ Add a property +- ✅ Add a tenant +- ✅ Create a lease +- ✅ Generate a rent invoice +- ✅ Record a payment +- ✅ Schedule an inspection + +--- + +## 🧭 What's Next? + +### Explore More Features + +**Property Management:** + +- Add multiple properties (up to 9 in SimpleStart) +- Upload property documents (certificates, insurance, photos) +- Mark properties as Under Renovation or Off Market +- Track property value and appreciation + +**Tenant Workflow:** + +- Use the **Prospect-to-Tenant** journey for new tenants: + 1. Add Prospect (inquiry phase) + 2. Schedule Tour + 3. Submit Rental Application + 4. Screen Application (background/credit checks) + 5. Approve/Deny Application + 6. Generate Lease Offer + 7. Tenant Accepts Lease + 8. Automatic Tenant Creation + +**Financial Management:** + +- Set up **recurring rent invoices** (auto-generated monthly) +- Configure **late fees** (grace period + amount/percentage) +- Generate **financial reports** (income, expenses, payment history) +- Track **security deposits** and annual dividends + +**Maintenance & Inspections:** + +- Create **maintenance requests** from tenants +- Assign requests to vendors +- Track repair costs and completion +- Schedule **routine inspections** (quarterly, semi-annual, annual) +- Generate inspection reports + +**Calendar & Scheduling:** + +- View all events in **monthly calendar** +- Schedule **property tours** for prospects +- Track **lease expiration dates** +- Set **payment due date reminders** + +**Notifications:** + +- Configure **email notifications** (SendGrid) +- Enable **SMS alerts** (Twilio) +- Customize **notification preferences** per user +- View **notification history** in Notification Center + +**Background Automation:** + +- Let the system automatically: + - Apply late fees after grace period + - Send lease expiration warnings (60/30/14 days) + - Calculate and distribute security deposit dividends + - Schedule routine inspections + - Clean up old data + +--- + +## 📚 Additional Resources + +### Documentation + +- **Release Notes** - What's new in v1.0.0 +- **User Guide** - Comprehensive 10-chapter guide covering all features +- **Administrator Guide** - System configuration and management +- **Database Management Guide** - Backup, restore, troubleshooting + +### Getting Help + +**Support Channels:** + +- 📧 **Email Support:** cisguru@outlook.com +- 🐛 **Report Bugs:** [GitHub Issues](https://github.com/xnodeoncode/Aquiis/issues) +- 💡 **Request Features:** [GitHub Discussions](https://github.com/xnodeoncode/Aquiis/discussions) +- 📖 **Documentation:** `/Documentation/v1.0.0/` + +**Community:** + +- Star the project on GitHub +- Contribute to development +- Share feedback and suggestions + +--- + +## ⚙️ Settings & Configuration + +### Essential Settings to Configure + +**Organization Settings:** + +1. Navigate to **Settings** → **Organization** +2. Update business hours, contact information, logo + +**User Settings:** + +1. Navigate to **Settings** → **Profile** +2. Update personal information, password, notification preferences + +**Notification Settings:** + +1. Navigate to **Settings** → **Notifications** +2. Enable/disable email, SMS, in-app notifications +3. Configure SendGrid (email) and Twilio (SMS) API keys if needed + +**Database Settings:** + +1. Navigate to **Settings** → **Database** +2. Configure **automatic backups:** + - Enable scheduled backups + - Set backup frequency (daily, weekly, monthly) + - Choose backup time (default: 2 AM) +3. Monitor database health and size + +**Financial Settings:** + +1. Navigate to **Settings** → **Financial** +2. Configure **late fees:** + - Grace period (default: 5 days) + - Late fee amount ($50 or 5% of rent) + - Apply late fees automatically: Yes/No + +**Inspection Settings:** + +1. Navigate to **Settings** → **Inspections** +2. Configure **routine inspection frequency:** + - None, Quarterly, Semi-Annual, Annual +3. Enable auto-scheduling of move-in inspections + +--- + +## 🔒 Security & Data Protection + +### Best Practices + +**Password Management:** + +- Use a **strong, unique password** +- Enable **password manager** for secure storage +- Change password periodically + +**Data Backups:** + +- Enable **automatic daily backups** +- Store backups in **multiple locations** (local + cloud) +- Test restore process periodically + +**Session Security:** + +- Application auto-locks after **18 minutes of inactivity** +- Always **log out** when leaving computer unattended + +**Data Privacy:** + +- All tenant data encrypted at rest +- Soft delete enabled (data recoverable if accidentally deleted) +- Audit trails track all data changes (who, when, what) + +--- + +## ❓ Common Questions + +### Q: Can I import data from another property management system? + +**A:** Not directly in v1.0.0. You'll need to manually enter your properties, tenants, and leases. We're working on data import features for v1.1.0. + +### Q: What happens when I reach the 9-property limit? + +**A:** When you try to add a 10th property, you'll see an upgrade message explaining that SimpleStart is limited to 9 properties. You can either: + +- Remove an inactive property to add a new one +- Upgrade to Aquiis Professional (future release) for unlimited properties + +### Q: Can I have more than 3 users? + +**A:** No, SimpleStart is limited to 3 users (1 system account + 2 login users). This is a product-level restriction. For more users, you'll need to upgrade to Aquiis Professional when available. + +### Q: Is my data stored in the cloud? + +**A:** No, Aquiis SimpleStart stores all data **locally on your computer** in a SQLite database file. Your data never leaves your device unless you choose to enable email/SMS notifications or back up to a cloud service. + +### Q: Do I need internet access? + +**A:** **No** for core features - the application works completely offline. **Yes** for optional features: + +- Email notifications (SendGrid) +- SMS notifications (Twilio) +- Future online payment processing + +### Q: How do I back up my data? + +**A:** Navigate to **Settings** → **Database** → **Backup & Restore**: + +- Click **"Create Backup"** for manual backup (recommended before major changes) +- Enable **"Schedule Automatic Backups"** for daily/weekly backups +- Backups stored in: `Data/Backups/` folder +- Copy backups to external drive or cloud storage for safety + +### Q: What if I accidentally delete something? + +**A:** SimpleStart uses **soft delete** - deleted records are marked as deleted but not permanently removed. Contact support for data recovery assistance if needed. + +### Q: Can I run this on a Mac? + +**A:** Not in v1.0.0. macOS support is planned for a future release. For now, use a Windows or Linux computer. + +### Q: Is there a mobile app? + +**A:** Not yet. A mobile companion app (view-only) is planned for v1.1.0. + +--- + +## 🐛 Troubleshooting + +### Application won't start + +**Windows:** + +- Right-click installer → "Run as Administrator" +- Check if antivirus is blocking the application +- Ensure .NET 10 Runtime is installed (bundled with app) + +**Linux:** + +- Ensure AppImage has execute permissions: `chmod +x Aquiis*.AppImage` +- Check system logs: `journalctl -xe` +- Verify dependencies installed (usually auto-included) + +### Database connection error + +1. Navigate to **Settings** → **Database** +2. Click **"Check Database Health"** +3. If corrupted, click **"Restore from Backup"** +4. If no backup, click **"Reset Database"** (⚠️ loses all data) + +### Can't log in + +- Verify email and password are correct +- Check Caps Lock is off +- If forgotten password, use **"Reset Password"** link +- If still stuck, check logs in `Data/Logs/` folder + +### Email notifications not working + +1. Navigate to **Settings** → **Notifications** +2. Verify SendGrid API key is correct +3. Check SendGrid account is active and not rate-limited +4. View error logs in **Settings** → **System** → **Logs** + +### Application is slow + +- Check database size: **Settings** → **Database** → **Database Size** +- Run database optimization: **Settings** → **Database** → **Optimize** +- Close other applications to free up memory +- Consider hardware upgrade if below minimum specs + +--- + +## 📞 Support + +Need help? We're here for you! + +**Email:** cisguru@outlook.com +**GitHub:** [https://github.com/xnodeoncode/Aquiis](https://github.com/xnodeoncode/Aquiis) + +**When contacting support, please include:** + +1. **Version number** (Settings → About) +2. **Operating system** (Windows/Linux) +3. **Description of issue** (what happened vs what you expected) +4. **Steps to reproduce** (how to recreate the problem) +5. **Screenshots** (if applicable) +6. **Log files** (Settings → System → Export Logs) + +--- + +## 🎓 Next Steps + +**Ready to dive deeper?** + +1. **Read the User Guide** - Comprehensive 10-chapter guide covering every feature +2. **Configure automation** - Set up late fees, recurring invoices, backups +3. **Explore reports** - Generate financial and operational reports +4. **Customize settings** - Tailor the application to your workflow +5. **Join the community** - Connect with other landlords using Aquiis + +**Thank you for choosing Aquiis SimpleStart!** 🏠 + +We hope this Quick Start Guide helped you get up and running quickly. Enjoy managing your properties with confidence! + +--- + +**Document Version:** 1.0 +**Last Updated:** January 28, 2026 +**Author:** CIS Guru with GitHub Copilot diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a1fcd71 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 CIS Guru + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 2204cee415d4463f5d58bbbd5d169173bca4e613 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Thu, 29 Jan 2026 20:14:33 -0600 Subject: [PATCH 02/10] Minor documentation edits. --- 4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj | 8 ++++---- Documentation/Quick-Start-Guide.md | 8 ++++++++ README.md | 8 ++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj b/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj index b4e4f6d..e46c78d 100644 --- a/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj +++ b/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj @@ -9,10 +9,10 @@ Data/Migrations - 1.0.0 - 1.0.0.0 - 1.0.0.0 - 1.0.0 + 1.0.1 + 1.0.1.0 + 1.0.1.0 + 1.0.1 diff --git a/Documentation/Quick-Start-Guide.md b/Documentation/Quick-Start-Guide.md index 4dc79a0..82c919f 100644 --- a/Documentation/Quick-Start-Guide.md +++ b/Documentation/Quick-Start-Guide.md @@ -35,6 +35,14 @@ Before you begin, ensure you have: - 500 MB disk space - **Downloaded:** Aquiis SimpleStart v1.0.0 installer for your platform +### Universal Linux Support: + +Aquiis is distributed as an AppImage, which runs on all major Linux distributions—including Ubuntu, Debian, Fedora, RedHat, Arch, openSUSE, and more. No installation required: just download, make executable, and run. + +### Windows Portable Version: + +Aquiis is available as a portable Windows executable (.exe). No installation required—just download, extract, and run. All application data is stored locally in the same folder, making it easy to use Aquiis from a USB drive or move between systems. + --- ## 🚀 Step 1: Installation (5 minutes) diff --git a/README.md b/README.md index 366b407..51a768a 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,14 @@ Follow our [Quick Start Guide](Documentation/v1.0.0/v1.0.0-Quick-Start-Guide.md) - **RAM:** 2 GB - **Disk:** 500 MB +### Universal Linux Support: + +Aquiis is distributed as an AppImage, which runs on all major Linux distributions—including Ubuntu, Debian, Fedora, RedHat, Arch, openSUSE, and more. No installation required: just download, make executable, and run. + +### Windows Portable Version: + +Aquiis is available as a portable Windows executable (.exe). No installation required—just download, extract, and run. All application data is stored locally in the same folder, making it easy to use Aquiis from a USB drive or move between systems. + ### Recommended - **CPU:** 4-core, 2.5 GHz From cfe61c820fdedb821b6d1069d9ff7375451bf390 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 31 Jan 2026 17:14:47 -0600 Subject: [PATCH 03/10] Add offline protection for AppImageHub compliance - Created offline.html with auto-retry functionality - Added backend health check before window load - Falls back to offline page if backend unavailable - Meets AppImageHub offline requirement for edge cases --- 4-Aquiis.SimpleStart/Program.cs | 23 ++++ 4-Aquiis.SimpleStart/wwwroot/offline.html | 145 ++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 4-Aquiis.SimpleStart/wwwroot/offline.html diff --git a/4-Aquiis.SimpleStart/Program.cs b/4-Aquiis.SimpleStart/Program.cs index dcf6599..1dde00f 100644 --- a/4-Aquiis.SimpleStart/Program.cs +++ b/4-Aquiis.SimpleStart/Program.cs @@ -555,6 +555,22 @@ // Open Electron window if (HybridSupport.IsElectronActive) { + // Verify backend is responding before showing window + var backendUrl = "http://localhost:8888"; + var isBackendReady = false; + + try + { + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + var response = await httpClient.GetAsync(backendUrl); + isBackendReady = response.IsSuccessStatusCode; + app.Logger.LogInformation("Backend health check: {Status}", isBackendReady ? "OK" : "Failed"); + } + catch (Exception ex) + { + app.Logger.LogWarning(ex, "Backend health check failed, will show offline page"); + } + var window = await Electron.WindowManager.CreateWindowAsync(new ElectronNET.API.Entities.BrowserWindowOptions { Width = 1400, @@ -567,6 +583,13 @@ window.OnReadyToShow += () => window.Show(); window.SetTitle("Aquiis Property Management"); + // Load appropriate page based on backend availability + if (!isBackendReady) + { + app.Logger.LogWarning("Loading offline page due to backend unavailability"); + window.LoadURL($"{backendUrl}/offline.html"); + } + // Open DevTools in development mode for debugging if (app.Environment.IsDevelopment()) { diff --git a/4-Aquiis.SimpleStart/wwwroot/offline.html b/4-Aquiis.SimpleStart/wwwroot/offline.html new file mode 100644 index 0000000..a4ca903 --- /dev/null +++ b/4-Aquiis.SimpleStart/wwwroot/offline.html @@ -0,0 +1,145 @@ + + + + + + Aquiis - Connection Error + + + +
+
⚠️
+

Connection Error

+

+ Aquiis is unable to start the local application server. This typically + indicates a temporary issue with the application. +

+ + + +
+ +
+ Status: Application server not responding
+ Expected: http://localhost:8888 +
+
+ + + + From 6153115e13a9021f8affdeab8d03429db13be844 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sun, 1 Feb 2026 13:59:46 -0600 Subject: [PATCH 04/10] security enhancements --- .../Aquiis.SimpleStart.csproj | 8 +- .../Extensions/ElectronServiceExtensions.cs | 27 +++-- .../Extensions/WebServiceExtensions.cs | 17 +++- .../Features/Calendar/Calendar.razor | 98 ++++++++++++++++-- .../Features/Calendar/CalendarListView.razor | 38 ++++++- 4-Aquiis.SimpleStart/Program.cs | 99 ++++++++++++++++++- .../Services/ElectronPathService.cs | 81 ++++++++++++++- .../Components/Account/Pages/Login.razor | 17 +++- .../appsettings.Production.json | 15 +++ 4-Aquiis.SimpleStart/appsettings.json | 2 +- 4-Aquiis.SimpleStart/electron.manifest.json | 2 - .../Aquiis.Professional.csproj | 8 +- .../Extensions/ElectronServiceExtensions.cs | 27 +++-- .../Extensions/WebServiceExtensions.cs | 17 +++- .../Features/Calendar/Calendar.razor | 98 ++++++++++++++++-- .../Features/Calendar/CalendarListView.razor | 38 ++++++- 5-Aquiis.Professional/Program.cs | 81 ++++++++++++++- .../Services/ElectronPathService.cs | 81 ++++++++++++++- .../Components/Account/Pages/Login.razor | 17 +++- .../appsettings.Production.json | 15 +++ 5-Aquiis.Professional/appsettings.json | 2 +- bump-version.sh | 4 +- copilot-review-to-backlog.sh | 62 +++++++++--- 23 files changed, 776 insertions(+), 78 deletions(-) create mode 100644 4-Aquiis.SimpleStart/appsettings.Production.json create mode 100644 5-Aquiis.Professional/appsettings.Production.json diff --git a/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj b/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj index e46c78d..eb73404 100644 --- a/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj +++ b/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj @@ -9,10 +9,10 @@ Data/Migrations - 1.0.1 - 1.0.1.0 - 1.0.1.0 - 1.0.1 + 1.1.0 + 1.1.0.0 + 1.1.0.0 + 1.1.0 diff --git a/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs index 41753e3..16f5e05 100644 --- a/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs +++ b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs @@ -31,8 +31,8 @@ public static IServiceCollection AddElectronServices( // Register path service services.AddScoped(); - // Get connection string using the path service - var connectionString = GetElectronConnectionString(configuration).GetAwaiter().GetResult(); + // Get connection string using the path service (synchronous to avoid startup deadlock) + var connectionString = GetElectronConnectionString(configuration); // ✅ Register Application layer (includes Infrastructure internally) services.AddApplication(connectionString); @@ -54,11 +54,14 @@ public static IServiceCollection AddElectronServices( services.AddIdentity(options => { // For desktop app, simplify registration (email confirmation can be enabled later via settings) options.SignIn.RequireConfirmedAccount = false; // Electron mode + + // ✅ SECURITY: Strong password policy (12+ chars, special characters required) options.Password.RequireDigit = true; - options.Password.RequiredLength = 6; - options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 12; + options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequireLowercase = true; + options.Password.RequiredUniqueChars = 4; // Prevent patterns like "aaa111!!!" }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); @@ -73,17 +76,27 @@ public static IServiceCollection AddElectronServices( // For Electron desktop app, use longer cookie lifetime options.ExpireTimeSpan = TimeSpan.FromDays(30); options.SlidingExpiration = true; + + // Ensure cookie is persisted (not session-only) + options.Cookie.MaxAge = TimeSpan.FromDays(30); + options.Cookie.IsEssential = true; + + // For localhost Electron app, allow non-HTTPS cookies + options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest; + options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax; }); return services; } /// - /// Gets the connection string for Electron mode using the path service. + /// Gets the connection string for Electron mode using the path service synchronously. + /// This avoids deadlocks during service registration before Electron is fully initialized. /// - private static async Task GetElectronConnectionString(IConfiguration configuration) + private static string GetElectronConnectionString(IConfiguration configuration) { var pathService = new ElectronPathService(configuration); - return await pathService.GetConnectionStringAsync(configuration); + var dbPath = pathService.GetDatabasePathSync(); + return $"DataSource={dbPath};Cache=Shared"; } } diff --git a/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs index 363ce47..bd010ce 100644 --- a/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs +++ b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs @@ -31,9 +31,13 @@ public static IServiceCollection AddWebServices( // Register path service services.AddScoped(); - // Get connection string from configuration - var connectionString = configuration.GetConnectionString("DefaultConnection") - ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + // ✅ SECURITY: Get connection string from environment variable first (production), + // then fall back to configuration (development) + var connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING") + ?? configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException( + "Connection string not found. " + + "Set DATABASE_CONNECTION_STRING environment variable or configure DefaultConnection in appsettings.json"); // ✅ Register Application layer (includes Infrastructure internally) services.AddApplication(connectionString); @@ -55,11 +59,14 @@ public static IServiceCollection AddWebServices( services.AddIdentity(options => { // For web app, require confirmed email options.SignIn.RequireConfirmedAccount = true; + + // ✅ SECURITY: Strong password policy (12+ chars, special characters required) options.Password.RequireDigit = true; - options.Password.RequiredLength = 6; - options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 12; + options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequireLowercase = true; + options.Password.RequiredUniqueChars = 4; // Prevent patterns like "aaa111!!!" }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); diff --git a/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor b/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor index 8ef89a0..6c9d26f 100644 --- a/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor +++ b/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor @@ -21,6 +21,7 @@ @inject TourService TourService @inject InspectionService InspectionService @inject MaintenanceService MaintenanceService +@inject IJSRuntime JSRuntime @rendermode InteractiveServer @@ -97,6 +98,14 @@
} + else if (viewMode == null) + { +
+
+ Loading... +
+
+ } else { @@ -637,7 +646,7 @@ private Inspection? selectedInspection; private MaintenanceRequest? selectedMaintenanceRequest; private bool loading = true; - private string viewMode = "month"; // day, week, month + private string? viewMode = null; // Will be loaded from localStorage, defaults to "month" if not set private DateTime currentDate = DateTime.Today; private List selectedEventTypes = new(); private bool showFilters = false; @@ -647,6 +656,7 @@ private List propertySearchResults = new(); private bool showPropertySearchResults = false; private Property? selectedPropertyForEvent = null; + private bool viewModeLoaded = false; protected override async Task OnInitializedAsync() { @@ -667,7 +677,49 @@ selectedEventTypes = CalendarEventTypes.GetAllTypes().ToList(); } - await LoadEvents(); + // Don't load events here - wait until view mode is loaded from localStorage + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !viewModeLoaded) + { + viewModeLoaded = true; + + // Check if user prefers list view - redirect if so + var savedPagePreference = await JSRuntime.InvokeAsync("localStorage.getItem", "calendarViewPage"); + await JSRuntime.InvokeVoidAsync("console.log", $"Retrieved calendarViewPage: {savedPagePreference ?? "null"}"); + + if (savedPagePreference == "list") + { + await JSRuntime.InvokeVoidAsync("console.log", "User prefers list view, redirecting..."); + Navigation.NavigateTo("/Calendar/ListView"); + return; // Stop execution, we're navigating away + } + + // User is staying on grid view - update preference + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewPage", "grid"); + + // Load saved view mode from localStorage + var savedViewMode = await JSRuntime.InvokeAsync("localStorage.getItem", "calendarViewMode"); + await JSRuntime.InvokeVoidAsync("console.log", $"Retrieved calendarViewMode: {savedViewMode ?? "null"}"); + + if (!string.IsNullOrEmpty(savedViewMode) && (savedViewMode == "day" || savedViewMode == "week" || savedViewMode == "month")) + { + viewMode = savedViewMode; + await JSRuntime.InvokeVoidAsync("console.log", $"Applied saved view mode: {savedViewMode}"); + } + else + { + // Default to month view if no preference saved + viewMode = "month"; + await JSRuntime.InvokeVoidAsync("console.log", "No saved preference, defaulting to month view"); + } + + // Now load events with the correct view mode + await LoadEvents(); + StateHasChanged(); + } } private async Task LoadEvents() @@ -678,8 +730,9 @@ var organizationId = await UserContext.GetActiveOrganizationIdAsync(); if (organizationId.HasValue) { - // Get date range based on current view - var (startDate, endDate) = viewMode switch + // Get date range based on current view (default to month if not set) + var currentView = viewMode ?? "month"; + var (startDate, endDate) = currentView switch { "day" => (currentDate.Date, currentDate.Date.AddDays(1)), "week" => (GetWeekStart(), GetWeekEnd().AddDays(1)), @@ -851,12 +904,28 @@ private async Task ChangeView(string mode) { viewMode = mode; + + // Persist view mode preference to localStorage + try + { + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewMode", mode); + // Log to browser console for debugging + await JSRuntime.InvokeVoidAsync("console.log", $"Saved view mode to localStorage: {mode}"); + } + catch (Exception ex) + { + // Show error to user if localStorage save fails + ToastService.ShowError($"Failed to save view mode preference: {ex.Message}"); + await JSRuntime.InvokeVoidAsync("console.error", $"localStorage.setItem error: {ex.Message}"); + } + await LoadEvents(); } private async Task NavigatePrevious() { - currentDate = viewMode switch + var currentView = viewMode ?? "month"; + currentDate = currentView switch { "day" => currentDate.AddDays(-1), "week" => currentDate.AddDays(-7), @@ -868,7 +937,8 @@ private async Task NavigateNext() { - currentDate = viewMode switch + var currentView = viewMode ?? "month"; + currentDate = currentView switch { "day" => currentDate.AddDays(1), "week" => currentDate.AddDays(7), @@ -886,7 +956,8 @@ private string GetDateRangeTitle() { - return viewMode switch + var currentView = viewMode ?? "month"; + return currentView switch { "day" => currentDate.ToString("dddd, MMMM dd, yyyy"), "week" => $"{GetWeekStart().ToString("MMM dd")} - {GetWeekEnd().ToString("MMM dd, yyyy")}", @@ -1567,8 +1638,19 @@ Navigation.NavigateTo("/PropertyManagement/ProspectiveTenants"); } - private void NavigateToListView() + private async void NavigateToListView() { + // Save preference for list view before navigating + try + { + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewPage", "list"); + await JSRuntime.InvokeVoidAsync("console.log", "Saved preference: calendarViewPage = list"); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("console.error", $"Failed to save list view preference: {ex.Message}"); + } + Navigation.NavigateTo("/Calendar/ListView"); } diff --git a/4-Aquiis.SimpleStart/Features/Calendar/CalendarListView.razor b/4-Aquiis.SimpleStart/Features/Calendar/CalendarListView.razor index 7d03818..69f002f 100644 --- a/4-Aquiis.SimpleStart/Features/Calendar/CalendarListView.razor +++ b/4-Aquiis.SimpleStart/Features/Calendar/CalendarListView.razor @@ -19,6 +19,7 @@ @inject InspectionService InspectionService @inject MaintenanceService MaintenanceService @inject LeaseService LeaseService +@inject IJSRuntime JSRuntime @rendermode InteractiveServer @@ -457,6 +458,7 @@ private int currentPage = 1; private int pageSize = 20; private int totalPages => (int)Math.Ceiling(filteredEvents.Count / (double)pageSize); + private bool firstRenderCompleted = false; protected override async Task OnInitializedAsync() { @@ -471,6 +473,29 @@ ApplyFilters(); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !firstRenderCompleted) + { + firstRenderCompleted = true; + + // Check if user prefers grid view - redirect if so + var savedPagePreference = await JSRuntime.InvokeAsync("localStorage.getItem", "calendarViewPage"); + await JSRuntime.InvokeVoidAsync("console.log", $"Retrieved calendarViewPage: {savedPagePreference ?? "null"}"); + + if (savedPagePreference == "grid") + { + await JSRuntime.InvokeVoidAsync("console.log", "User prefers grid view, redirecting..."); + Navigation.NavigateTo("/Calendar"); + return; // Stop execution, we're navigating away + } + + // User is staying on list view - update preference + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewPage", "list"); + await JSRuntime.InvokeVoidAsync("console.log", "Saved preference: calendarViewPage = list"); + } + } + private async Task LoadEvents() { try @@ -543,8 +568,19 @@ showFilters = !showFilters; } - private void NavigateToCalendar() + private async void NavigateToCalendar() { + // Save preference for grid view before navigating back + try + { + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewPage", "grid"); + await JSRuntime.InvokeVoidAsync("console.log", "Saved preference: calendarViewPage = grid"); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("console.error", $"Failed to save grid view preference: {ex.Message}"); + } + Navigation.NavigateTo("/Calendar"); } diff --git a/4-Aquiis.SimpleStart/Program.cs b/4-Aquiis.SimpleStart/Program.cs index 1dde00f..e575e41 100644 --- a/4-Aquiis.SimpleStart/Program.cs +++ b/4-Aquiis.SimpleStart/Program.cs @@ -214,6 +214,55 @@ { var pathService = scope.ServiceProvider.GetRequiredService(); var dbPath = await pathService.GetDatabasePathAsync(); + + // ✅ v1.1.0: Automatic migration from old Electron folder to new Aquiis folder + var basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + basePath = Environment.GetEnvironmentVariable("HOME")!; + basePath = OperatingSystem.IsLinux() + ? Path.Combine(basePath, ".config") + : Path.Combine(basePath, "Library/Application Support"); + } + + var dbFileName = Path.GetFileName(dbPath); + var oldDbPath = Path.Combine(basePath, "Electron", dbFileName); + var oldBackupPath = Path.Combine(basePath, "Electron", "Backups"); + var newBackupPath = Path.Combine(Path.GetDirectoryName(dbPath)!, "Backups"); + + // One-time migration: copy database and backups if old location exists and new doesn't + if (File.Exists(oldDbPath) && !File.Exists(dbPath)) + { + app.Logger.LogInformation("Migrating database from Electron folder to Aquiis folder"); + app.Logger.LogInformation("Old path: {OldPath}", oldDbPath); + app.Logger.LogInformation("New path: {NewPath}", dbPath); + + // Ensure destination directory exists + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + + // Copy database file + File.Copy(oldDbPath, dbPath); + app.Logger.LogInformation("Database file migrated successfully"); + + // Copy backups folder if it exists + if (Directory.Exists(oldBackupPath)) + { + app.Logger.LogInformation("Migrating backups folder"); + Directory.CreateDirectory(newBackupPath); + + var backupFiles = Directory.GetFiles(oldBackupPath); + foreach (var backupFile in backupFiles) + { + var destFile = Path.Combine(newBackupPath, Path.GetFileName(backupFile)); + File.Copy(backupFile, destFile); + } + + app.Logger.LogInformation("Migrated {Count} backup files", backupFiles.Length); + } + + app.Logger.LogInformation("Database migration from Electron to Aquiis folder completed successfully"); + } + var stagedRestorePath = $"{dbPath}.restore_pending"; // Check if there's a staged restore waiting @@ -486,10 +535,38 @@ app.UseSession(); -// Only use HTTPS redirection in web mode, not in Electron +// ✅ SECURITY: HTTPS enforcement for production web mode if (!HybridSupport.IsElectronActive) { - app.UseHttpsRedirection(); + if (!app.Environment.IsDevelopment()) + { + // Production: MUST use HTTPS + app.UseHttpsRedirection(); + app.UseHsts(); + + // Validate HTTPS is actually configured + var httpsUrl = builder.Configuration["Kestrel:Endpoints:Https:Url"]; + if (string.IsNullOrEmpty(httpsUrl)) + { + app.Logger.LogWarning( + "HTTPS not configured in production. " + + "Configure Kestrel:Endpoints:Https in appsettings.Production.json or set ASPNETCORE_URLS environment variable."); + } + } + else + { + // Development: Optional HTTPS (for testing) + var useHttps = builder.Configuration.GetValue("Development:UseHttps", false); + if (useHttps) + { + app.UseHttpsRedirection(); + app.Logger.LogInformation("HTTPS enabled for development"); + } + else + { + app.Logger.LogInformation("Running in development without HTTPS"); + } + } } app.UseAuthentication(); @@ -550,7 +627,23 @@ } // Start the app for Electron -await app.StartAsync(); +try +{ + app.Logger.LogInformation("Starting ASP.NET Core server..."); + await app.StartAsync(); + app.Logger.LogInformation("ASP.NET Core server started successfully"); +} +catch (Exception ex) +{ + app.Logger.LogCritical(ex, "FATAL: Failed to start ASP.NET Core server"); + Console.WriteLine($"FATAL ERROR: {ex.Message}"); + Console.WriteLine($"Stack Trace: {ex.StackTrace}"); + if (ex.InnerException != null) + { + Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); + } + Environment.Exit(1); +} // Open Electron window if (HybridSupport.IsElectronActive) diff --git a/4-Aquiis.SimpleStart/Services/ElectronPathService.cs b/4-Aquiis.SimpleStart/Services/ElectronPathService.cs index d462718..c9c07c5 100644 --- a/4-Aquiis.SimpleStart/Services/ElectronPathService.cs +++ b/4-Aquiis.SimpleStart/Services/ElectronPathService.cs @@ -59,12 +59,91 @@ public async Task GetDatabasePathAsync() } } + /// + /// Gets the database path synchronously (for startup initialization before Electron is ready). + /// + public string GetDatabasePathSync() + { + var dbFileName = _configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; + + if (HybridSupport.IsElectronActive) + { + // Use OS-specific user data path without requiring Electron to be initialized + var userDataPath = GetUserDataPathSync(); + var dbPath = Path.Combine(userDataPath, dbFileName); + + // Ensure the directory exists + var directory = Path.GetDirectoryName(dbPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + return dbPath; + } + else + { + // Fallback to local path if not in Electron mode + var dataDir = Path.Combine(Directory.GetCurrentDirectory(), "Data"); + if (!Directory.Exists(dataDir)) + { + Directory.CreateDirectory(dataDir); + } + return Path.Combine(dataDir, dbFileName); + } + } + /// public async Task GetUserDataPathAsync() { if (HybridSupport.IsElectronActive) { - return await Electron.App.GetPathAsync(PathName.UserData); + // Use sync method to ensure consistent path resolution + // This matches the startup behavior and uses "Aquiis" as the app name + return GetUserDataPathSync(); + } + else + { + // Fallback for non-Electron mode + return Path.Combine(Directory.GetCurrentDirectory(), "Data"); + } + } + + /// + /// Gets the user data path synchronously. + /// + private string GetUserDataPathSync() + { + if (HybridSupport.IsElectronActive) + { + // Determine OS-specific user data path without Electron API + string basePath; + var appName = "Aquiis"; + + if (OperatingSystem.IsWindows()) + { + basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + } + else if (OperatingSystem.IsMacOS()) + { + basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", "Application Support"); + } + else // Linux + { + basePath = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); + } + + var userDataPath = Path.Combine(basePath, appName); + + // Ensure directory exists + if (!Directory.Exists(userDataPath)) + { + Directory.CreateDirectory(userDataPath); + } + + return userDataPath; } else { diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Login.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Login.razor index 797ea24..2a44cd1 100644 --- a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Login.razor +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Login.razor @@ -3,6 +3,7 @@ @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Identity +@using ElectronNET.API @inject SignInManager SignInManager @inject ILogger Logger @@ -77,12 +78,24 @@ protected override async Task OnInitializedAsync() { - await base.OnInitializedAsync(); + Input ??= new InputModel(); - Input = Input ?? new InputModel(); + // For Electron, default Remember Me to true (trusted device) + if (HybridSupport.IsElectronActive) + { + Input.RememberMe = true; + } if (HttpMethods.IsGet(HttpContext.Request.Method)) { + // Check if user is already authenticated via Remember Me cookie + if (HttpContext.User.Identity?.IsAuthenticated == true) + { + // Already logged in, redirect to return URL or home + RedirectManager.RedirectTo(ReturnUrl ?? "/"); + return; + } + // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); } diff --git a/4-Aquiis.SimpleStart/appsettings.Production.json b/4-Aquiis.SimpleStart/appsettings.Production.json new file mode 100644 index 0000000..9820158 --- /dev/null +++ b/4-Aquiis.SimpleStart/appsettings.Production.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Development": { + "UseHttps": false + }, + "ConnectionStrings": { + "__comment": "⚠️ DO NOT store production connection strings here. Use DATABASE_CONNECTION_STRING environment variable instead." + }, + "__comment_kestrel": "⚠️ Kestrel endpoints removed from appsettings.Production.json - these should be configured via ASPNETCORE_URLS environment variable or command line for web deployments only. Electron mode uses its own port configuration (8888)." +} diff --git a/4-Aquiis.SimpleStart/appsettings.json b/4-Aquiis.SimpleStart/appsettings.json index e710202..e8d9507 100644 --- a/4-Aquiis.SimpleStart/appsettings.json +++ b/4-Aquiis.SimpleStart/appsettings.json @@ -14,7 +14,7 @@ "ApplicationSettings": { "AppName": "Aquiis", "ProductName": "Aquiis SimpleStart", - "Version": "1.0.1", + "Version": "1.1.0", "Author": "CIS Guru", "Email": "cisguru@outlook.com", "Repository": "https://github.com/xnodeoncode/Aquiis", diff --git a/4-Aquiis.SimpleStart/electron.manifest.json b/4-Aquiis.SimpleStart/electron.manifest.json index 53e0fab..fe964f3 100644 --- a/4-Aquiis.SimpleStart/electron.manifest.json +++ b/4-Aquiis.SimpleStart/electron.manifest.json @@ -4,8 +4,6 @@ "imageFile": "wwwroot/assets/splash.png" }, "electronCLIFlags": [ - "--no-sandbox", - "--disable-gpu-sandbox", "--enable-features=VaapiVideoDecoder", "--disable-dev-shm-usage" ], diff --git a/5-Aquiis.Professional/Aquiis.Professional.csproj b/5-Aquiis.Professional/Aquiis.Professional.csproj index ce47093..f9b1cef 100644 --- a/5-Aquiis.Professional/Aquiis.Professional.csproj +++ b/5-Aquiis.Professional/Aquiis.Professional.csproj @@ -9,10 +9,10 @@ Data/Migrations - 0.2.0 - 0.2.0.0 - 0.2.0.0 - 0.2.0 + 0.3.1 + 0.3.1.0 + 0.3.1.0 + 0.3.1 diff --git a/5-Aquiis.Professional/Extensions/ElectronServiceExtensions.cs b/5-Aquiis.Professional/Extensions/ElectronServiceExtensions.cs index 06ad2a5..d77a598 100644 --- a/5-Aquiis.Professional/Extensions/ElectronServiceExtensions.cs +++ b/5-Aquiis.Professional/Extensions/ElectronServiceExtensions.cs @@ -31,8 +31,8 @@ public static IServiceCollection AddElectronServices( // Register path service services.AddScoped(); - // Get connection string using the path service - var connectionString = GetElectronConnectionString(configuration).GetAwaiter().GetResult(); + // Get connection string using the path service (synchronous to avoid startup deadlock) + var connectionString = GetElectronConnectionString(configuration); // ✅ Register Application layer (includes Infrastructure internally) services.AddApplication(connectionString); @@ -54,11 +54,14 @@ public static IServiceCollection AddElectronServices( services.AddIdentity(options => { // For desktop app, simplify registration (email confirmation can be enabled later via settings) options.SignIn.RequireConfirmedAccount = false; // Electron mode + + // ✅ SECURITY: Strong password policy (12+ chars, special characters required) options.Password.RequireDigit = true; - options.Password.RequiredLength = 6; - options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 12; + options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequireLowercase = true; + options.Password.RequiredUniqueChars = 4; // Prevent patterns like "aaa111!!!" }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); @@ -73,17 +76,27 @@ public static IServiceCollection AddElectronServices( // For Electron desktop app, use longer cookie lifetime options.ExpireTimeSpan = TimeSpan.FromDays(30); options.SlidingExpiration = true; + + // Ensure cookie is persisted (not session-only) + options.Cookie.MaxAge = TimeSpan.FromDays(30); + options.Cookie.IsEssential = true; + + // For localhost Electron app, allow non-HTTPS cookies + options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest; + options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax; }); return services; } /// - /// Gets the connection string for Electron mode using the path service. + /// Gets the connection string for Electron mode using the path service synchronously. + /// This avoids deadlocks during service registration before Electron is fully initialized. /// - private static async Task GetElectronConnectionString(IConfiguration configuration) + private static string GetElectronConnectionString(IConfiguration configuration) { var pathService = new ElectronPathService(configuration); - return await pathService.GetConnectionStringAsync(configuration); + var dbPath = pathService.GetDatabasePathSync(); + return $"DataSource={dbPath};Cache=Shared"; } } diff --git a/5-Aquiis.Professional/Extensions/WebServiceExtensions.cs b/5-Aquiis.Professional/Extensions/WebServiceExtensions.cs index 0c0c302..5c920a6 100644 --- a/5-Aquiis.Professional/Extensions/WebServiceExtensions.cs +++ b/5-Aquiis.Professional/Extensions/WebServiceExtensions.cs @@ -31,9 +31,13 @@ public static IServiceCollection AddWebServices( // Register path service services.AddScoped(); - // Get connection string from configuration - var connectionString = configuration.GetConnectionString("DefaultConnection") - ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + // ✅ SECURITY: Get connection string from environment variable first (production), + // then fall back to configuration (development) + var connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING") + ?? configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException( + "Connection string not found. " + + "Set DATABASE_CONNECTION_STRING environment variable or configure DefaultConnection in appsettings.json"); // ✅ Register Application layer (includes Infrastructure internally) services.AddApplication(connectionString); @@ -55,11 +59,14 @@ public static IServiceCollection AddWebServices( services.AddIdentity(options => { // For web app, require confirmed email options.SignIn.RequireConfirmedAccount = true; + + // ✅ SECURITY: Strong password policy (12+ chars, special characters required) options.Password.RequireDigit = true; - options.Password.RequiredLength = 6; - options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 12; + options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequireLowercase = true; + options.Password.RequiredUniqueChars = 4; // Prevent patterns like "aaa111!!!" }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); diff --git a/5-Aquiis.Professional/Features/Calendar/Calendar.razor b/5-Aquiis.Professional/Features/Calendar/Calendar.razor index 59c094a..3c7d6a6 100644 --- a/5-Aquiis.Professional/Features/Calendar/Calendar.razor +++ b/5-Aquiis.Professional/Features/Calendar/Calendar.razor @@ -21,6 +21,7 @@ @inject TourService TourService @inject InspectionService InspectionService @inject MaintenanceService MaintenanceService +@inject IJSRuntime JSRuntime @rendermode InteractiveServer @@ -97,6 +98,14 @@
} + else if (viewMode == null) + { +
+
+ Loading... +
+
+ } else { @@ -637,7 +646,7 @@ private Inspection? selectedInspection; private MaintenanceRequest? selectedMaintenanceRequest; private bool loading = true; - private string viewMode = "week"; // day, week, month + private string? viewMode = null; // Will be loaded from localStorage, defaults to "week" if not set private DateTime currentDate = DateTime.Today; private List selectedEventTypes = new(); private bool showFilters = false; @@ -647,6 +656,7 @@ private List propertySearchResults = new(); private bool showPropertySearchResults = false; private Property? selectedPropertyForEvent = null; + private bool viewModeLoaded = false; protected override async Task OnInitializedAsync() { @@ -667,7 +677,49 @@ selectedEventTypes = CalendarEventTypes.GetAllTypes().ToList(); } - await LoadEvents(); + // Don't load events here - wait until view mode is loaded from localStorage + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !viewModeLoaded) + { + viewModeLoaded = true; + + // Check if user prefers list view - redirect if so + var savedPagePreference = await JSRuntime.InvokeAsync("localStorage.getItem", "calendarViewPage"); + await JSRuntime.InvokeVoidAsync("console.log", $"Retrieved calendarViewPage: {savedPagePreference ?? "null"}"); + + if (savedPagePreference == "list") + { + await JSRuntime.InvokeVoidAsync("console.log", "User prefers list view, redirecting..."); + Navigation.NavigateTo("/Calendar/ListView"); + return; // Stop execution, we're navigating away + } + + // User is staying on grid view - update preference + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewPage", "grid"); + + // Load saved view mode from localStorage + var savedViewMode = await JSRuntime.InvokeAsync("localStorage.getItem", "calendarViewMode"); + await JSRuntime.InvokeVoidAsync("console.log", $"Retrieved calendarViewMode: {savedViewMode ?? "null"}"); + + if (!string.IsNullOrEmpty(savedViewMode) && (savedViewMode == "day" || savedViewMode == "week" || savedViewMode == "month")) + { + viewMode = savedViewMode; + await JSRuntime.InvokeVoidAsync("console.log", $"Applied saved view mode: {savedViewMode}"); + } + else + { + // Default to week view if no preference saved (Professional edition default) + viewMode = "week"; + await JSRuntime.InvokeVoidAsync("console.log", "No saved preference, defaulting to week view"); + } + + // Now load events with the correct view mode + await LoadEvents(); + StateHasChanged(); + } } private async Task LoadEvents() @@ -678,8 +730,9 @@ var organizationId = await UserContext.GetActiveOrganizationIdAsync(); if (organizationId.HasValue) { - // Get date range based on current view - var (startDate, endDate) = viewMode switch + // Get date range based on current view (default to week if not set - Professional default) + var currentView = viewMode ?? "week"; + var (startDate, endDate) = currentView switch { "day" => (currentDate.Date, currentDate.Date.AddDays(1)), "week" => (GetWeekStart(), GetWeekEnd().AddDays(1)), @@ -851,12 +904,28 @@ private async Task ChangeView(string mode) { viewMode = mode; + + // Persist view mode preference to localStorage + try + { + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewMode", mode); + // Log to browser console for debugging + await JSRuntime.InvokeVoidAsync("console.log", $"Saved view mode to localStorage: {mode}"); + } + catch (Exception ex) + { + // Show error to user if localStorage save fails + ToastService.ShowError($"Failed to save view mode preference: {ex.Message}"); + await JSRuntime.InvokeVoidAsync("console.error", $"localStorage.setItem error: {ex.Message}"); + } + await LoadEvents(); } private async Task NavigatePrevious() { - currentDate = viewMode switch + var currentView = viewMode ?? "week"; + currentDate = currentView switch { "day" => currentDate.AddDays(-1), "week" => currentDate.AddDays(-7), @@ -868,7 +937,8 @@ private async Task NavigateNext() { - currentDate = viewMode switch + var currentView = viewMode ?? "week"; + currentDate = currentView switch { "day" => currentDate.AddDays(1), "week" => currentDate.AddDays(7), @@ -886,7 +956,8 @@ private string GetDateRangeTitle() { - return viewMode switch + var currentView = viewMode ?? "week"; + return currentView switch { "day" => currentDate.ToString("dddd, MMMM dd, yyyy"), "week" => $"{GetWeekStart().ToString("MMM dd")} - {GetWeekEnd().ToString("MMM dd, yyyy")}", @@ -1567,8 +1638,19 @@ Navigation.NavigateTo("/PropertyManagement/ProspectiveTenants"); } - private void NavigateToListView() + private async void NavigateToListView() { + // Save preference for list view before navigating + try + { + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewPage", "list"); + await JSRuntime.InvokeVoidAsync("console.log", "Saved preference: calendarViewPage = list"); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("console.error", $"Failed to save list view preference: {ex.Message}"); + } + Navigation.NavigateTo("/Calendar/ListView"); } diff --git a/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor b/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor index 6e10dd8..c428e3e 100644 --- a/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor +++ b/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor @@ -19,6 +19,7 @@ @inject InspectionService InspectionService @inject MaintenanceService MaintenanceService @inject LeaseService LeaseService +@inject IJSRuntime JSRuntime @rendermode InteractiveServer @@ -457,6 +458,7 @@ private int currentPage = 1; private int pageSize = 20; private int totalPages => (int)Math.Ceiling(filteredEvents.Count / (double)pageSize); + private bool firstRenderCompleted = false; protected override async Task OnInitializedAsync() { @@ -471,6 +473,29 @@ ApplyFilters(); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !firstRenderCompleted) + { + firstRenderCompleted = true; + + // Check if user prefers grid view - redirect if so + var savedPagePreference = await JSRuntime.InvokeAsync("localStorage.getItem", "calendarViewPage"); + await JSRuntime.InvokeVoidAsync("console.log", $"Retrieved calendarViewPage: {savedPagePreference ?? "null"}"); + + if (savedPagePreference == "grid") + { + await JSRuntime.InvokeVoidAsync("console.log", "User prefers grid view, redirecting..."); + Navigation.NavigateTo("/Calendar"); + return; // Stop execution, we're navigating away + } + + // User is staying on list view - update preference + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewPage", "list"); + await JSRuntime.InvokeVoidAsync("console.log", "Saved preference: calendarViewPage = list"); + } + } + private async Task LoadEvents() { try @@ -543,8 +568,19 @@ showFilters = !showFilters; } - private void NavigateToCalendar() + private async void NavigateToCalendar() { + // Save preference for grid view before navigating back + try + { + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "calendarViewPage", "grid"); + await JSRuntime.InvokeVoidAsync("console.log", "Saved preference: calendarViewPage = grid"); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("console.error", $"Failed to save grid view preference: {ex.Message}"); + } + Navigation.NavigateTo("/Calendar"); } diff --git a/5-Aquiis.Professional/Program.cs b/5-Aquiis.Professional/Program.cs index afc4117..432611f 100644 --- a/5-Aquiis.Professional/Program.cs +++ b/5-Aquiis.Professional/Program.cs @@ -213,6 +213,55 @@ { var pathService = scope.ServiceProvider.GetRequiredService(); var dbPath = await pathService.GetDatabasePathAsync(); + + // ✅ v0.3.1: Automatic migration from old Electron folder to new Aquiis folder + var basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + basePath = Environment.GetEnvironmentVariable("HOME")!; + basePath = OperatingSystem.IsLinux() + ? Path.Combine(basePath, ".config") + : Path.Combine(basePath, "Library/Application Support"); + } + + var dbFileName = Path.GetFileName(dbPath); + var oldDbPath = Path.Combine(basePath, "Electron", dbFileName); + var oldBackupPath = Path.Combine(basePath, "Electron", "Backups"); + var newBackupPath = Path.Combine(Path.GetDirectoryName(dbPath)!, "Backups"); + + // One-time migration: copy database and backups if old location exists and new doesn't + if (File.Exists(oldDbPath) && !File.Exists(dbPath)) + { + app.Logger.LogInformation("Migrating database from Electron folder to Aquiis folder"); + app.Logger.LogInformation("Old path: {OldPath}", oldDbPath); + app.Logger.LogInformation("New path: {NewPath}", dbPath); + + // Ensure destination directory exists + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + + // Copy database file + File.Copy(oldDbPath, dbPath); + app.Logger.LogInformation("Database file migrated successfully"); + + // Copy backups folder if it exists + if (Directory.Exists(oldBackupPath)) + { + app.Logger.LogInformation("Migrating backups folder"); + Directory.CreateDirectory(newBackupPath); + + var backupFiles = Directory.GetFiles(oldBackupPath); + foreach (var backupFile in backupFiles) + { + var destFile = Path.Combine(newBackupPath, Path.GetFileName(backupFile)); + File.Copy(backupFile, destFile); + } + + app.Logger.LogInformation("Migrated {Count} backup files", backupFiles.Length); + } + + app.Logger.LogInformation("Database migration from Electron to Aquiis folder completed successfully"); + } + var stagedRestorePath = $"{dbPath}.restore_pending"; // Check if there's a staged restore waiting @@ -485,10 +534,38 @@ app.UseSession(); -// Only use HTTPS redirection in web mode, not in Electron +// ✅ SECURITY: HTTPS enforcement for production web mode if (!HybridSupport.IsElectronActive) { - app.UseHttpsRedirection(); + if (!app.Environment.IsDevelopment()) + { + // Production: MUST use HTTPS + app.UseHttpsRedirection(); + app.UseHsts(); + + // Validate HTTPS is actually configured + var httpsUrl = builder.Configuration["Kestrel:Endpoints:Https:Url"]; + if (string.IsNullOrEmpty(httpsUrl)) + { + app.Logger.LogWarning( + "HTTPS not configured in production. " + + "Configure Kestrel:Endpoints:Https in appsettings.Production.json or set ASPNETCORE_URLS environment variable."); + } + } + else + { + // Development: Optional HTTPS (for testing) + var useHttps = builder.Configuration.GetValue("Development:UseHttps", false); + if (useHttps) + { + app.UseHttpsRedirection(); + app.Logger.LogInformation("HTTPS enabled for development"); + } + else + { + app.Logger.LogInformation("Running in development without HTTPS"); + } + } } app.UseAuthentication(); diff --git a/5-Aquiis.Professional/Services/ElectronPathService.cs b/5-Aquiis.Professional/Services/ElectronPathService.cs index 4478074..a389a5b 100644 --- a/5-Aquiis.Professional/Services/ElectronPathService.cs +++ b/5-Aquiis.Professional/Services/ElectronPathService.cs @@ -59,12 +59,91 @@ public async Task GetDatabasePathAsync() } } + /// + /// Gets the database path synchronously (for startup initialization before Electron is ready). + /// + public string GetDatabasePathSync() + { + var dbFileName = _configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; + + if (HybridSupport.IsElectronActive) + { + // Use OS-specific user data path without requiring Electron to be initialized + var userDataPath = GetUserDataPathSync(); + var dbPath = Path.Combine(userDataPath, dbFileName); + + // Ensure the directory exists + var directory = Path.GetDirectoryName(dbPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + return dbPath; + } + else + { + // Fallback to local path if not in Electron mode + var dataDir = Path.Combine(Directory.GetCurrentDirectory(), "Data"); + if (!Directory.Exists(dataDir)) + { + Directory.CreateDirectory(dataDir); + } + return Path.Combine(dataDir, dbFileName); + } + } + /// public async Task GetUserDataPathAsync() { if (HybridSupport.IsElectronActive) { - return await Electron.App.GetPathAsync(PathName.UserData); + // Use sync method to ensure consistent path resolution + // This matches the startup behavior and uses "Aquiis" as the app name + return GetUserDataPathSync(); + } + else + { + // Fallback for non-Electron mode + return Path.Combine(Directory.GetCurrentDirectory(), "Data"); + } + } + + /// + /// Gets the user data path synchronously. + /// + private string GetUserDataPathSync() + { + if (HybridSupport.IsElectronActive) + { + // Determine OS-specific user data path without Electron API + string basePath; + var appName = "Aquiis"; + + if (OperatingSystem.IsWindows()) + { + basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + } + else if (OperatingSystem.IsMacOS()) + { + basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", "Application Support"); + } + else // Linux + { + basePath = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); + } + + var userDataPath = Path.Combine(basePath, appName); + + // Ensure directory exists + if (!Directory.Exists(userDataPath)) + { + Directory.CreateDirectory(userDataPath); + } + + return userDataPath; } else { diff --git a/5-Aquiis.Professional/Shared/Components/Account/Pages/Login.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Login.razor index b55359a..5516805 100644 --- a/5-Aquiis.Professional/Shared/Components/Account/Pages/Login.razor +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Login.razor @@ -3,6 +3,7 @@ @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Identity +@using ElectronNET.API @inject SignInManager SignInManager @inject ILogger Logger @@ -77,12 +78,24 @@ protected override async Task OnInitializedAsync() { - await base.OnInitializedAsync(); + Input ??= new InputModel(); - Input = Input ?? new InputModel(); + // For Electron, default Remember Me to true (trusted device) + if (HybridSupport.IsElectronActive) + { + Input.RememberMe = true; + } if (HttpMethods.IsGet(HttpContext.Request.Method)) { + // Check if user is already authenticated via Remember Me cookie + if (HttpContext.User.Identity?.IsAuthenticated == true) + { + // Already logged in, redirect to return URL or home + RedirectManager.RedirectTo(ReturnUrl ?? "/"); + return; + } + // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); } diff --git a/5-Aquiis.Professional/appsettings.Production.json b/5-Aquiis.Professional/appsettings.Production.json new file mode 100644 index 0000000..9820158 --- /dev/null +++ b/5-Aquiis.Professional/appsettings.Production.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Development": { + "UseHttps": false + }, + "ConnectionStrings": { + "__comment": "⚠️ DO NOT store production connection strings here. Use DATABASE_CONNECTION_STRING environment variable instead." + }, + "__comment_kestrel": "⚠️ Kestrel endpoints removed from appsettings.Production.json - these should be configured via ASPNETCORE_URLS environment variable or command line for web deployments only. Electron mode uses its own port configuration (8888)." +} diff --git a/5-Aquiis.Professional/appsettings.json b/5-Aquiis.Professional/appsettings.json index 40fc2ad..45564ea 100644 --- a/5-Aquiis.Professional/appsettings.json +++ b/5-Aquiis.Professional/appsettings.json @@ -13,7 +13,7 @@ "AllowedHosts": "*", "ApplicationSettings": { "AppName": "Aquiis", - "Version": "0.3.0", + "Version": "0.3.1", "Author": "CIS Guru", "Email": "cisguru@outlook.com", "Repository": "https://github.com/xnodeoncode/Aquiis", diff --git a/bump-version.sh b/bump-version.sh index 797f61e..0274c52 100755 --- a/bump-version.sh +++ b/bump-version.sh @@ -6,8 +6,8 @@ set -e VERSION_TYPE="${1:-patch}" -CSPROJ_FILE="Aquiis.SimpleStart/Aquiis.SimpleStart.csproj" -APPSETTINGS_FILE="Aquiis.SimpleStart/appsettings.json" +CSPROJ_FILE="4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj" +APPSETTINGS_FILE="4-Aquiis.SimpleStart/appsettings.json" # Colors for output RED='\033[0;31m' diff --git a/copilot-review-to-backlog.sh b/copilot-review-to-backlog.sh index 6b5b5af..1edaef8 100755 --- a/copilot-review-to-backlog.sh +++ b/copilot-review-to-backlog.sh @@ -4,6 +4,10 @@ # Usage: ./copilot-review-to-backlog.sh set -e +set -o pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +pushd "$SCRIPT_DIR" > /dev/null PR_NUMBER=$1 # Use absolute path to BACKLOG.md @@ -22,6 +26,13 @@ if ! command -v gh &> /dev/null; then exit 1 fi +# Check if jq is installed (used for parsing JSON from gh api) +if ! command -v jq &> /dev/null; then + echo "Error: jq is not installed." + echo "Install with: sudo dnf install jq" + exit 1 +fi + # Check if user is authenticated if ! gh auth status &> /dev/null; then echo "Error: Not authenticated with GitHub CLI" @@ -34,6 +45,10 @@ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) PR_TITLE=$(gh pr view $PR_NUMBER --json title -q .title) PR_URL=$(gh pr view $PR_NUMBER --json url -q .url) +# GitHub uses different usernames for Copilot depending on endpoint/context. +# Examples seen in the wild: "Copilot", "copilot-pull-request-reviewer", "github-copilot[bot]". +COPILOT_LOGIN_REGEX='^(Copilot|copilot-pull-request-reviewer|github-copilot\[bot\])$' + echo "Fetching Copilot review suggestions from PR #$PR_NUMBER..." # Debug: Check what review authors exist @@ -41,26 +56,42 @@ echo "Debug: Checking review authors..." gh pr view $PR_NUMBER --json reviews --jq '.reviews[].author.login' | sort -u # Fetch Copilot review comments (copilot-pull-request-reviewer is the actual bot name) -COMMENTS=$(gh api repos/$REPO/pulls/$PR_NUMBER/comments --jq ' - .[] - | select(.user.login == "copilot-pull-request-reviewer") - | "**File:** `\(.path)` (Line \(.line))\n\n\(.body)\n" +# Format: File, Line, Body, and code suggestion (if present) +COMMENTS=$(gh api repos/$REPO/pulls/$PR_NUMBER/comments | jq -r --arg re "$COPILOT_LOGIN_REGEX" ' + .[] + | select(.user.login | test($re; "i")) + | "**File:** \(.path) (Line \((.line // .original_line // .position // "?")|tostring))\n" + + (if .html_url then "**Link:** \(.html_url)\n" else "" end) + + (if .body then "**Comment:**\n" + .body + "\n" else "" end) + + "---\n" +') + +# Fetch Copilot PR conversation comments (e.g., PR overview comment) +PR_COMMENTS=$(gh api repos/$REPO/issues/$PR_NUMBER/comments | jq -r --arg re "$COPILOT_LOGIN_REGEX" ' + .[] + | select(.user.login | test($re; "i")) + | "**Link:** \(.html_url)\n" + + "**Comment:**\n" + (.body // "") + "\n" + + "---\n" ') # Fetch Copilot review body (overall review) -REVIEW=$(gh pr view $PR_NUMBER --json reviews --jq ' - .reviews[] - | select(.author.login == "copilot-pull-request-reviewer") - | .body -' | head -1) +REVIEW=$(gh pr view $PR_NUMBER --json reviews | jq -r --arg re "$COPILOT_LOGIN_REGEX" ' + ( + .reviews + | map(select((.author.login // .user.login // "") | test($re; "i"))) + | first + | .body + ) // "" +') # If still no results, try fetching ALL review comments to see structure -if [ -z "$COMMENTS" ] && [ -z "$REVIEW" ]; then +if [ -z "$COMMENTS" ] && [ -z "$PR_COMMENTS" ] && [ -z "$REVIEW" ]; then echo "Debug: No Copilot reviews found. Checking all reviews..." gh api repos/$REPO/pulls/$PR_NUMBER/reviews --jq '.[] | {author: .user.login, body: .body}' | head -20 fi -if [ -z "$COMMENTS" ] && [ -z "$REVIEW" ]; then +if [ -z "$COMMENTS" ] && [ -z "$PR_COMMENTS" ] && [ -z "$REVIEW" ]; then echo "No Copilot review suggestions found on PR #$PR_NUMBER" exit 0 fi @@ -82,6 +113,13 @@ $REVIEW " fi +if [ -n "$PR_COMMENTS" ]; then + ENTRY+="### PR Conversation Comments + +$PR_COMMENTS +" +fi + if [ -n "$COMMENTS" ]; then ENTRY+="### Inline Suggestions @@ -104,3 +142,5 @@ echo "$ENTRY" >> "$BACKLOG_FILE" echo "✅ Copilot review suggestions appended to BACKLOG.md" echo "📄 Review PR #$PR_NUMBER at: $PR_URL" echo "📝 Backlog updated: $BACKLOG_FILE" + +popd > /dev/null From 21c52aeef1a3ac06232bc24778798a01f489639a Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Mon, 2 Feb 2026 15:31:18 -0600 Subject: [PATCH 05/10] database encryption for electron. --- 0-Aquiis.Core/Entities/DatabaseSettings.cs | 45 + .../Interfaces/Services/IDatabaseService.cs | 17 + .../Aquiis.Infrastructure.csproj | 1 + .../Data/ApplicationDbContext.cs | 1 + ...0201231400_AddDatabaseSettings.Designer.cs | 4270 ++++++++++++++++ .../20260201231400_AddDatabaseSettings.cs | 38 + ...260201234216_AddEncryptionSalt.Designer.cs | 4274 +++++++++++++++++ .../20260201234216_AddEncryptionSalt.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 29 + .../Data/SqlCipherConnectionInterceptor.cs | 104 + .../DependencyInjection.cs | 21 +- .../Services/DatabaseEncryptionService.cs | 310 ++ .../Services/LinuxKeychainService.cs | 169 + .../Services/PasswordDerivationService.cs | 103 + 2-Aquiis.Application/DependencyInjection.cs | 9 +- .../Services/DatabasePasswordService.cs | 101 + .../Services/DatabaseService.cs | 54 + .../Extensions/ElectronServiceExtensions.cs | 51 +- .../Extensions/SecurityHeadersMiddleware.cs | 127 + .../Extensions/WebServiceExtensions.cs | 131 +- .../Settings/Pages/DatabaseSettings.razor | 457 +- 4-Aquiis.SimpleStart/Program.cs | 158 +- .../Components/PasswordInputModal.razor | 226 + .../Shared/Services/DatabaseBackupService.cs | 65 + .../Extensions/SecurityHeadersMiddleware.cs | 127 + 5-Aquiis.Professional/Program.cs | 3 + .../NewSetupUITests.cs | 16 +- .../NewSetupUITests.cs | 6 +- Documentation/Compatibility-Matrix.md | 258 + 29 files changed, 11127 insertions(+), 73 deletions(-) create mode 100644 0-Aquiis.Core/Entities/DatabaseSettings.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260201231400_AddDatabaseSettings.Designer.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260201231400_AddDatabaseSettings.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260201234216_AddEncryptionSalt.Designer.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260201234216_AddEncryptionSalt.cs create mode 100644 1-Aquiis.Infrastructure/Data/SqlCipherConnectionInterceptor.cs create mode 100644 1-Aquiis.Infrastructure/Services/DatabaseEncryptionService.cs create mode 100644 1-Aquiis.Infrastructure/Services/LinuxKeychainService.cs create mode 100644 1-Aquiis.Infrastructure/Services/PasswordDerivationService.cs create mode 100644 2-Aquiis.Application/Services/DatabasePasswordService.cs create mode 100644 4-Aquiis.SimpleStart/Extensions/SecurityHeadersMiddleware.cs create mode 100644 4-Aquiis.SimpleStart/Shared/Components/PasswordInputModal.razor create mode 100644 5-Aquiis.Professional/Extensions/SecurityHeadersMiddleware.cs create mode 100644 Documentation/Compatibility-Matrix.md diff --git a/0-Aquiis.Core/Entities/DatabaseSettings.cs b/0-Aquiis.Core/Entities/DatabaseSettings.cs new file mode 100644 index 0000000..4590884 --- /dev/null +++ b/0-Aquiis.Core/Entities/DatabaseSettings.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.Core.Entities +{ + /// + /// Database-level settings that affect all organizations. + /// These are runtime configuration values stored in the database itself. + /// + public class DatabaseSettings + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// + /// Database encryption status - applies to the entire SQLite file + /// + [Required] + public bool DatabaseEncryptionEnabled { get; set; } = false; + + /// + /// When encryption status was last changed + /// + public DateTime? EncryptionChangedOn { get; set; } + + /// + /// Salt used for password-derived key (base64 encoded) + /// Required for portable encryption - same password + salt = same key + /// + [StringLength(256)] + public string? EncryptionSalt { get; set; } + + /// + /// Last time settings were modified + /// + public DateTime LastModifiedOn { get; set; } = DateTime.UtcNow; + + /// + /// User or system that last modified settings + /// + [StringLength(128)] + public string LastModifiedBy { get; set; } = "System"; + } +} diff --git a/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs b/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs index 380c5fd..9bc3a99 100644 --- a/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs +++ b/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs @@ -1,3 +1,5 @@ +using Aquiis.Core.Entities; + namespace Aquiis.Core.Interfaces.Services; /// @@ -25,4 +27,19 @@ public interface IDatabaseService /// Get count of pending migrations for identity context /// Task GetIdentityPendingMigrationsCountAsync(); + + /// + /// Get database settings (creates default if not exists) + /// + Task GetDatabaseSettingsAsync(); + + /// + /// Set database encryption status + /// + Task SetDatabaseEncryptionAsync(bool enabled, string modifiedBy = "System"); + + /// + /// Check if database encryption is currently enabled + /// + Task IsDatabaseEncryptionEnabledAsync(); } diff --git a/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj b/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj index cd859f5..3653bfa 100644 --- a/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj +++ b/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj @@ -16,6 +16,7 @@ + diff --git a/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs index 47b36fa..ef1dedf 100644 --- a/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs +++ b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs @@ -72,6 +72,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) public DbSet Repairs { get; set; } public DbSet OrganizationSettings { get; set; } public DbSet SchemaVersions { get; set; } + public DbSet DatabaseSettings { get; set; } public DbSet ChecklistTemplates { get; set; } public DbSet ChecklistTemplateItems { get; set; } public DbSet Checklists { get; set; } diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260201231400_AddDatabaseSettings.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260201231400_AddDatabaseSettings.Designer.cs new file mode 100644 index 0000000..51a89c7 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260201231400_AddDatabaseSettings.Designer.cs @@ -0,0 +1,4270 @@ +// +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.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260201231400_AddDatabaseSettings")] + partial class AddDatabaseSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + 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("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 11, + ItemText = "Showed water heater location", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 18, + ItemText = "Explained pet policy", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Document property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Inspect property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Prepare information packets", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.DatabaseSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DatabaseEncryptionEnabled") + .HasColumnType("INTEGER"); + + b.Property("EncryptionChangedOn") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DatabaseSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyDigestTime") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmailApplicationStatusChange") + .HasColumnType("INTEGER"); + + b.Property("EmailInspectionScheduled") + .HasColumnType("INTEGER"); + + b.Property("EmailLeaseExpiring") + .HasColumnType("INTEGER"); + + b.Property("EmailMaintenanceUpdate") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentReceived") + .HasColumnType("INTEGER"); + + b.Property("EnableDailyDigest") + .HasColumnType("INTEGER"); + + b.Property("EnableEmailNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableInAppNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableSMSNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableWeeklyDigest") + .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("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SMSLeaseExpiringUrgent") + .HasColumnType("INTEGER"); + + b.Property("SMSMaintenanceEmergency") + .HasColumnType("INTEGER"); + + b.Property("SMSPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WeeklyDigestDay") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("NotificationPreferences"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("DailyLimit") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentToday") + .HasColumnType("INTEGER"); + + b.Property("EnableSsl") + .HasColumnType("INTEGER"); + + b.Property("FromEmail") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FromName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsEmailEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastEmailSentOn") + .HasColumnType("TEXT"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastErrorOn") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyLimit") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendGridApiKeyEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SmtpPort") + .HasColumnType("INTEGER"); + + b.Property("SmtpServer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationEmailSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountBalance") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("AccountType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CostPerSMS") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSMSEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastSMSSentOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SMSSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("SMSSentToday") + .HasColumnType("INTEGER"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("TwilioAccountSidEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioAuthTokenEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioPhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSMSSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationUsers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("ContactId") + .HasColumnType("TEXT"); + + b.Property("ContactPerson") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorId") + .HasColumnType("TEXT"); + + b.Property("ContractorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Cost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MaintenanceRequestId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PartsReplaced") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RepairType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("WarrantyApplies") + .HasColumnType("INTEGER"); + + b.Property("WarrantyExpiresOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("LeaseId"); + + b.HasIndex("MaintenanceRequestId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RepairType"); + + b.ToTable("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActiveOrganizationId"); + + b.HasIndex("Email"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserProfiles"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EmailError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EmailSent") + .HasColumnType("INTEGER"); + + b.Property("EmailSentOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRead") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReadOn") + .HasColumnType("TEXT"); + + b.Property("RecipientUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RelatedEntityId") + .HasColumnType("TEXT"); + + b.Property("RelatedEntityType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SMSError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SMSSent") + .HasColumnType("INTEGER"); + + b.Property("SMSSentOn") + .HasColumnType("TEXT"); + + b.Property("SendEmail") + .HasColumnType("INTEGER"); + + b.Property("SendInApp") + .HasColumnType("INTEGER"); + + b.Property("SendSMS") + .HasColumnType("INTEGER"); + + b.Property("SentOn") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsRead"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("SentOn"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("MaintenanceRequests") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.MaintenanceRequest", "MaintenanceRequest") + .WithMany("Repairs") + .HasForeignKey("MaintenanceRequestId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Repairs") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("MaintenanceRequest"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + + b.Navigation("MaintenanceRequests"); + + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260201231400_AddDatabaseSettings.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260201231400_AddDatabaseSettings.cs new file mode 100644 index 0000000..6497d14 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260201231400_AddDatabaseSettings.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Infrastructure.Migrations +{ + /// + public partial class AddDatabaseSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DatabaseSettings", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DatabaseEncryptionEnabled = table.Column(type: "INTEGER", nullable: false), + EncryptionChangedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedOn = table.Column(type: "TEXT", nullable: false), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DatabaseSettings", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DatabaseSettings"); + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260201234216_AddEncryptionSalt.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260201234216_AddEncryptionSalt.Designer.cs new file mode 100644 index 0000000..49569c5 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260201234216_AddEncryptionSalt.Designer.cs @@ -0,0 +1,4274 @@ +// +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.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260201234216_AddEncryptionSalt")] + partial class AddEncryptionSalt + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + 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("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 11, + ItemText = "Showed water heater location", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 18, + ItemText = "Explained pet policy", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Document property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Inspect property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Prepare information packets", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.DatabaseSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DatabaseEncryptionEnabled") + .HasColumnType("INTEGER"); + + b.Property("EncryptionChangedOn") + .HasColumnType("TEXT"); + + b.Property("EncryptionSalt") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DatabaseSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyDigestTime") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmailApplicationStatusChange") + .HasColumnType("INTEGER"); + + b.Property("EmailInspectionScheduled") + .HasColumnType("INTEGER"); + + b.Property("EmailLeaseExpiring") + .HasColumnType("INTEGER"); + + b.Property("EmailMaintenanceUpdate") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentReceived") + .HasColumnType("INTEGER"); + + b.Property("EnableDailyDigest") + .HasColumnType("INTEGER"); + + b.Property("EnableEmailNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableInAppNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableSMSNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableWeeklyDigest") + .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("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SMSLeaseExpiringUrgent") + .HasColumnType("INTEGER"); + + b.Property("SMSMaintenanceEmergency") + .HasColumnType("INTEGER"); + + b.Property("SMSPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WeeklyDigestDay") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("NotificationPreferences"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("DailyLimit") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentToday") + .HasColumnType("INTEGER"); + + b.Property("EnableSsl") + .HasColumnType("INTEGER"); + + b.Property("FromEmail") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FromName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsEmailEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastEmailSentOn") + .HasColumnType("TEXT"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastErrorOn") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyLimit") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendGridApiKeyEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SmtpPort") + .HasColumnType("INTEGER"); + + b.Property("SmtpServer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationEmailSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountBalance") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("AccountType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CostPerSMS") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSMSEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastSMSSentOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SMSSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("SMSSentToday") + .HasColumnType("INTEGER"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("TwilioAccountSidEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioAuthTokenEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioPhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSMSSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationUsers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("ContactId") + .HasColumnType("TEXT"); + + b.Property("ContactPerson") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorId") + .HasColumnType("TEXT"); + + b.Property("ContractorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Cost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MaintenanceRequestId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PartsReplaced") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RepairType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("WarrantyApplies") + .HasColumnType("INTEGER"); + + b.Property("WarrantyExpiresOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("LeaseId"); + + b.HasIndex("MaintenanceRequestId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RepairType"); + + b.ToTable("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActiveOrganizationId"); + + b.HasIndex("Email"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserProfiles"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EmailError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EmailSent") + .HasColumnType("INTEGER"); + + b.Property("EmailSentOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRead") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReadOn") + .HasColumnType("TEXT"); + + b.Property("RecipientUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RelatedEntityId") + .HasColumnType("TEXT"); + + b.Property("RelatedEntityType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SMSError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SMSSent") + .HasColumnType("INTEGER"); + + b.Property("SMSSentOn") + .HasColumnType("TEXT"); + + b.Property("SendEmail") + .HasColumnType("INTEGER"); + + b.Property("SendInApp") + .HasColumnType("INTEGER"); + + b.Property("SendSMS") + .HasColumnType("INTEGER"); + + b.Property("SentOn") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsRead"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("SentOn"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("MaintenanceRequests") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.MaintenanceRequest", "MaintenanceRequest") + .WithMany("Repairs") + .HasForeignKey("MaintenanceRequestId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Repairs") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("MaintenanceRequest"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + + b.Navigation("MaintenanceRequests"); + + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260201234216_AddEncryptionSalt.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260201234216_AddEncryptionSalt.cs new file mode 100644 index 0000000..9435b4a --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260201234216_AddEncryptionSalt.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Infrastructure.Migrations +{ + /// + public partial class AddEncryptionSalt : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EncryptionSalt", + table: "DatabaseSettings", + type: "TEXT", + maxLength: 256, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EncryptionSalt", + table: "DatabaseSettings"); + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 87d374f..60dcd57 100644 --- a/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1080,6 +1080,35 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("Aquiis.Core.Entities.DatabaseSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DatabaseEncryptionEnabled") + .HasColumnType("INTEGER"); + + b.Property("EncryptionChangedOn") + .HasColumnType("TEXT"); + + b.Property("EncryptionSalt") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DatabaseSettings"); + }); + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => { b.Property("Id") diff --git a/1-Aquiis.Infrastructure/Data/SqlCipherConnectionInterceptor.cs b/1-Aquiis.Infrastructure/Data/SqlCipherConnectionInterceptor.cs new file mode 100644 index 0000000..445ad49 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/SqlCipherConnectionInterceptor.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore.Diagnostics; +using System.Data.Common; + +namespace Aquiis.Infrastructure.Data; + +/// +/// EF Core connection interceptor that sets SQLCipher encryption password after opening connections +/// +public class SqlCipherConnectionInterceptor : DbConnectionInterceptor +{ + private readonly string? _encryptionPassword; + + // Toggle for verbose logging (useful for troubleshooting encryption issues) + private const bool EnableVerboseLogging = true; + + public SqlCipherConnectionInterceptor(string? encryptionPassword) + { + _encryptionPassword = encryptionPassword; + if (EnableVerboseLogging) + Console.WriteLine($"[SqlCipherConnectionInterceptor] Initialized with password: {(_encryptionPassword != null ? $"YES (length: {_encryptionPassword.Length})" : "NO")}"); + } + + public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData) + { + if (EnableVerboseLogging) + Console.WriteLine($"[SqlCipherConnectionInterceptor] ConnectionOpened called - Database: {connection.Database}"); + + if (!string.IsNullOrEmpty(_encryptionPassword)) + { + using (var cmd = connection.CreateCommand()) + { + // CRITICAL: Set key FIRST, before any other PRAGMA commands + if (EnableVerboseLogging) + Console.WriteLine("[SqlCipherConnectionInterceptor] Setting encryption key..."); + cmd.CommandText = $"PRAGMA key = '{_encryptionPassword}';"; + cmd.ExecuteNonQuery(); + + // Now set SQLCipher 4 parameters + if (EnableVerboseLogging) + Console.WriteLine("[SqlCipherConnectionInterceptor] Setting SQLCipher 4 parameters..."); + cmd.CommandText = "PRAGMA cipher_page_size = 4096;"; + cmd.ExecuteNonQuery(); + + cmd.CommandText = "PRAGMA kdf_iter = 256000;"; + cmd.ExecuteNonQuery(); + + cmd.CommandText = "PRAGMA cipher_hmac_algorithm = HMAC_SHA512;"; + cmd.ExecuteNonQuery(); + + cmd.CommandText = "PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;"; + cmd.ExecuteNonQuery(); + + if (EnableVerboseLogging) + Console.WriteLine("[SqlCipherConnectionInterceptor] Encryption configured successfully"); + } + } + else if (EnableVerboseLogging) + { + Console.WriteLine("[SqlCipherConnectionInterceptor] No password provided, skipping encryption"); + } + base.ConnectionOpened(connection, eventData); + } + + public override async Task ConnectionOpenedAsync(DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default) + { + if (EnableVerboseLogging) + Console.WriteLine($"[SqlCipherConnectionInterceptor] ConnectionOpenedAsync called - Database: {connection.Database}"); + + if (!string.IsNullOrEmpty(_encryptionPassword)) + { + using (var cmd = connection.CreateCommand()) + { + // CRITICAL: Set key FIRST, before any other PRAGMA commands + if (EnableVerboseLogging) + Console.WriteLine("[SqlCipherConnectionInterceptor] Setting encryption key (async)..."); + cmd.CommandText = $"PRAGMA key = '{_encryptionPassword}';"; + await cmd.ExecuteNonQueryAsync(cancellationToken); + + // Now set SQLCipher 4 parameters + if (EnableVerboseLogging) + Console.WriteLine("[SqlCipherConnectionInterceptor] Setting SQLCipher 4 parameters (async)..."); + cmd.CommandText = "PRAGMA cipher_page_size = 4096;"; + await cmd.ExecuteNonQueryAsync(cancellationToken); + + cmd.CommandText = "PRAGMA kdf_iter = 256000;"; + await cmd.ExecuteNonQueryAsync(cancellationToken); + + cmd.CommandText = "PRAGMA cipher_hmac_algorithm = HMAC_SHA512;"; + await cmd.ExecuteNonQueryAsync(cancellationToken); + + cmd.CommandText = "PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;"; + await cmd.ExecuteNonQueryAsync(cancellationToken); + + if (EnableVerboseLogging) + Console.WriteLine("[SqlCipherConnectionInterceptor] Encryption configured successfully (async)"); + } + } + else if (EnableVerboseLogging) + { + Console.WriteLine("[SqlCipherConnectionInterceptor] No password provided, skipping encryption (async)"); + } + await base.ConnectionOpenedAsync(connection, eventData, cancellationToken); + } +} diff --git a/1-Aquiis.Infrastructure/DependencyInjection.cs b/1-Aquiis.Infrastructure/DependencyInjection.cs index 9616845..e167045 100644 --- a/1-Aquiis.Infrastructure/DependencyInjection.cs +++ b/1-Aquiis.Infrastructure/DependencyInjection.cs @@ -14,17 +14,26 @@ public static class DependencyInjection /// public static IServiceCollection AddInfrastructure( this IServiceCollection services, - string connectionString) + string connectionString, + string? encryptionPassword = null, + SqlCipherConnectionInterceptor? interceptor = null) { + // Configure DbContext options - use provided interceptor instance + Action configureOptions = options => + { + options.UseSqlite(connectionString); + if (interceptor != null) + { + options.AddInterceptors(interceptor); + } + }; + // Register ApplicationDbContext (business data) - services.AddDbContext(options => - options.UseSqlite(connectionString)); + services.AddDbContext(configureOptions); // Register DbContext factory for services that need it (like FinancialReportService) // Use AddDbContextFactory instead of AddPooledDbContextFactory to avoid lifetime issues - services.AddDbContextFactory(options => - options.UseSqlite(connectionString), - ServiceLifetime.Scoped); + services.AddDbContextFactory(configureOptions, ServiceLifetime.Scoped); // Register provider interfaces services.AddScoped(); diff --git a/1-Aquiis.Infrastructure/Services/DatabaseEncryptionService.cs b/1-Aquiis.Infrastructure/Services/DatabaseEncryptionService.cs new file mode 100644 index 0000000..2b91c69 --- /dev/null +++ b/1-Aquiis.Infrastructure/Services/DatabaseEncryptionService.cs @@ -0,0 +1,310 @@ +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace Aquiis.Infrastructure.Services; + +/// +/// Service for encrypting and decrypting SQLite databases using SQLCipher. +/// Handles the conversion between encrypted and unencrypted database files. +/// +public class DatabaseEncryptionService +{ + private readonly PasswordDerivationService _passwordDerivation; + private readonly LinuxKeychainService _keychain; + private readonly ILogger _logger; + + public DatabaseEncryptionService( + PasswordDerivationService passwordDerivation, + LinuxKeychainService keychain, + ILogger logger) + { + _passwordDerivation = passwordDerivation; + _keychain = keychain; + _logger = logger; + } + + /// + /// Encrypt an unencrypted database file + /// + /// Path to unencrypted database + /// User's master password (passed directly to SQLCipher) + /// (Success, EncryptedPath, ErrorMessage) + public async Task<(bool Success, string? EncryptedPath, string? ErrorMessage)> + EncryptDatabaseAsync(string sourcePath, string password) + { + try + { + _logger.LogInformation("Starting database encryption for {Path}", sourcePath); + + // Validate password + var (isValid, validationError) = _passwordDerivation.ValidatePasswordStrength(password); + if (!isValid) + { + return (false, null, validationError); + } + + // Create temporary encrypted database path + var encryptedPath = $"{sourcePath}.encrypted"; + + // Delete if exists from previous attempt + if (File.Exists(encryptedPath)) + { + File.Delete(encryptedPath); + } + + // Initialize SQLCipher + SQLitePCL.Batteries_V2.Init(); + SQLitePCL.raw.sqlite3_initialize(); + + // Attach and copy database using SQLCipher + using (var sourceConn = new SqliteConnection($"Data Source={sourcePath}")) + { + await sourceConn.OpenAsync(); + _logger.LogInformation("Source database opened successfully"); + + // Attach encrypted database with password (SQLCipher handles PBKDF2 internally) + using (var cmd = sourceConn.CreateCommand()) + { + cmd.CommandText = $"ATTACH DATABASE '{encryptedPath}' AS encrypted KEY '{password}';"; + await cmd.ExecuteNonQueryAsync(); + _logger.LogInformation("Encrypted database attached"); + } + + // Set SQLCipher 4 parameters for the attached database + using (var cmd = sourceConn.CreateCommand()) + { + cmd.CommandText = @" + PRAGMA encrypted.cipher_page_size = 4096; + PRAGMA encrypted.kdf_iter = 256000; + PRAGMA encrypted.cipher_hmac_algorithm = HMAC_SHA512; + PRAGMA encrypted.cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;"; + await cmd.ExecuteNonQueryAsync(); + _logger.LogInformation("SQLCipher parameters set"); + } + + // Export schema and data to encrypted database + using (var cmd = sourceConn.CreateCommand()) + { + cmd.CommandText = "SELECT sqlcipher_export('encrypted');"; + var result = await cmd.ExecuteScalarAsync(); + _logger.LogInformation("SQLCipher export completed: {Result}", result); + } + + // Detach encrypted database + using (var cmd = sourceConn.CreateCommand()) + { + cmd.CommandText = "DETACH DATABASE encrypted;"; + await cmd.ExecuteNonQueryAsync(); + _logger.LogInformation("Encrypted database detached"); + } + } + + _logger.LogInformation("Waiting for file system to settle after encryption"); + await Task.Delay(200); + + // Verify encrypted database can be opened + var verifySuccess = await VerifyEncryptedDatabaseAsync(encryptedPath, password); + if (!verifySuccess) + { + if (File.Exists(encryptedPath)) + { + File.Delete(encryptedPath); + } + return (false, null, "Failed to verify encrypted database"); + } + + // Store password in keychain (best effort - don't fail if keychain unavailable) + if (OperatingSystem.IsLinux()) + { + var stored = _keychain.StoreKey(password, "Aquiis Database Encryption Password"); + if (!stored) + { + _logger.LogWarning("Failed to store password in keychain - you'll need to enter it manually on next startup"); + } + else + { + _logger.LogInformation("Password stored in keychain successfully"); + } + } + + _logger.LogInformation("Database encryption completed successfully"); + return (true, encryptedPath, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to encrypt database"); + return (false, null, $"Encryption failed: {ex.Message}"); + } + } + + /// + /// Decrypt an encrypted database file + /// + /// Path to encrypted database + /// User's master password + /// (Success, DecryptedPath, ErrorMessage) + public async Task<(bool Success, string? DecryptedPath, string? ErrorMessage)> + DecryptDatabaseAsync(string encryptedPath, string password) + { + try + { + _logger.LogInformation("Starting database decryption for {Path}", encryptedPath); + + // Create temporary decrypted database path + var decryptedPath = $"{encryptedPath}.decrypted"; + + // Delete if exists from previous attempt + if (File.Exists(decryptedPath)) + { + File.Delete(decryptedPath); + } + + // Initialize SQLCipher + SQLitePCL.Batteries_V2.Init(); + SQLitePCL.raw.sqlite3_initialize(); + + // Open encrypted database and export to unencrypted + using (var encryptedConn = new SqliteConnection($"Data Source={encryptedPath}")) + { + await encryptedConn.OpenAsync(); + + // Set password using PRAGMA + using (var cmd = encryptedConn.CreateCommand()) + { + cmd.CommandText = $"PRAGMA key = '{password}';"; + await cmd.ExecuteNonQueryAsync(); + } + + // Attach unencrypted database + using (var cmd = encryptedConn.CreateCommand()) + { + cmd.CommandText = $"ATTACH DATABASE '{decryptedPath}' AS plaintext KEY '';"; + await cmd.ExecuteNonQueryAsync(); + } + + // Export schema and data to plaintext database + using (var cmd = encryptedConn.CreateCommand()) + { + cmd.CommandText = "SELECT sqlcipher_export('plaintext');"; + await cmd.ExecuteNonQueryAsync(); + } + + // Detach plaintext database + using (var cmd = encryptedConn.CreateCommand()) + { + cmd.CommandText = "DETACH DATABASE plaintext;"; + await cmd.ExecuteNonQueryAsync(); + } + } + + // Verify decrypted database can be opened + var verifySuccess = await VerifyPlaintextDatabaseAsync(decryptedPath); + if (!verifySuccess) + { + if (File.Exists(decryptedPath)) + { + File.Delete(decryptedPath); + } + return (false, null, "Failed to verify decrypted database"); + } + + // Remove password from keychain + if (OperatingSystem.IsLinux()) + { + _keychain.RemoveKey(); + } + + _logger.LogInformation("Database decryption completed successfully"); + return (true, decryptedPath, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to decrypt database"); + return (false, null, $"Decryption failed: {ex.Message}"); + } + } + + /// + /// Verify encrypted database can be opened with the provided password + /// + private async Task VerifyEncryptedDatabaseAsync(string dbPath, string password) + { + try + { + _logger.LogInformation("Verifying encrypted database at {Path}", dbPath); + using (var conn = new SqliteConnection($"Data Source={dbPath}")) + { + await conn.OpenAsync(); + + // Set the password using PRAGMA + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = $"PRAGMA key = '{password}';"; + await cmd.ExecuteNonQueryAsync(); + } + + _logger.LogInformation("Encrypted database opened successfully"); + + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT count(*) FROM sqlite_master;"; + var result = await cmd.ExecuteScalarAsync(); + _logger.LogInformation("Query executed successfully, result: {Result}", result); + return result != null; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to verify encrypted database"); + return false; + } + } + + /// + /// Verify plaintext database can be opened + /// + private async Task VerifyPlaintextDatabaseAsync(string dbPath) + { + try + { + using (var conn = new SqliteConnection($"Data Source={dbPath}")) + { + await conn.OpenAsync(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT count(*) FROM sqlite_master;"; + var result = await cmd.ExecuteScalarAsync(); + return result != null; + } + } + } + catch + { + return false; + } + } + + /// + /// Try to retrieve encryption key from keychain + /// + public string? TryGetKeyFromKeychain() + { + if (!OperatingSystem.IsLinux()) + return null; + + return _keychain.RetrieveKey(); + } + + /// + /// Check if keychain service is available + /// + public bool IsKeychainAvailable() + { + if (!OperatingSystem.IsLinux()) + return false; + + return _keychain.IsAvailable(); + } +} diff --git a/1-Aquiis.Infrastructure/Services/LinuxKeychainService.cs b/1-Aquiis.Infrastructure/Services/LinuxKeychainService.cs new file mode 100644 index 0000000..6491090 --- /dev/null +++ b/1-Aquiis.Infrastructure/Services/LinuxKeychainService.cs @@ -0,0 +1,169 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace Aquiis.Infrastructure.Services; + +/// +/// Service for storing and retrieving encryption keys from Linux Secret Service (libsecret). +/// Provides convenient auto-decryption on trusted devices. +/// +public class LinuxKeychainService +{ + private const string Schema = "org.aquiis.database"; + private const string KeyAttribute = "key-type"; + private const string KeyValue = "database-encryption"; + + /// + /// Store encryption key in Linux keychain (libsecret) + /// + /// Hex-encoded encryption key + /// Human-readable label for the key + /// True if stored successfully + public bool StoreKey(string keyHex, string label = "Aquiis Database Encryption Key") + { + if (!OperatingSystem.IsLinux()) + return false; + + try + { + // Use secret-tool command line utility (part of libsecret) + // This is more reliable than P/Invoke for cross-distribution compatibility + var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "secret-tool", + Arguments = $"store --label=\"{label}\" {KeyAttribute} {KeyValue}", + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + process.StandardInput.WriteLine(keyHex); + process.StandardInput.Close(); + process.WaitForExit(5000); + + return process.ExitCode == 0; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to store key in keychain: {ex.Message}"); + return false; + } + } + + /// + /// Retrieve encryption key from Linux keychain + /// + /// Hex-encoded encryption key, or null if not found + public string? RetrieveKey() + { + if (!OperatingSystem.IsLinux()) + return null; + + try + { + var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "secret-tool", + Arguments = $"lookup {KeyAttribute} {KeyValue}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) + { + return output.Trim(); + } + + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to retrieve key from keychain: {ex.Message}"); + return null; + } + } + + /// + /// Remove encryption key from Linux keychain + /// + /// True if removed successfully + public bool RemoveKey() + { + if (!OperatingSystem.IsLinux()) + return false; + + try + { + var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "secret-tool", + Arguments = $"clear {KeyAttribute} {KeyValue}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + process.WaitForExit(5000); + + return process.ExitCode == 0; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to remove key from keychain: {ex.Message}"); + return false; + } + } + + /// + /// Check if secret-tool is available on the system + /// + public bool IsAvailable() + { + if (!OperatingSystem.IsLinux()) + return false; + + try + { + var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "which", + Arguments = "secret-tool", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + process.WaitForExit(2000); + + return process.ExitCode == 0; + } + catch + { + return false; + } + } +} diff --git a/1-Aquiis.Infrastructure/Services/PasswordDerivationService.cs b/1-Aquiis.Infrastructure/Services/PasswordDerivationService.cs new file mode 100644 index 0000000..89c1885 --- /dev/null +++ b/1-Aquiis.Infrastructure/Services/PasswordDerivationService.cs @@ -0,0 +1,103 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Aquiis.Infrastructure.Services; + +/// +/// Service for deriving encryption keys from user passwords using PBKDF2. +/// Provides portable encryption - same password produces same key on any device. +/// +public class PasswordDerivationService +{ + private const int SaltSize = 32; // 256 bits + private const int KeySize = 32; // 256 bits for AES-256 + private const int Iterations = 600000; // OWASP recommendation for PBKDF2-SHA256 (2023+) + + /// + /// Generate a random salt for key derivation + /// + public byte[] GenerateSalt() + { + var salt = new byte[SaltSize]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + } + return salt; + } + + /// + /// Derive an encryption key from a password and salt + /// + /// User's master password + /// Random salt (should be stored with database) + /// 256-bit encryption key + public byte[] DeriveKey(string password, byte[] salt) + { + if (string.IsNullOrEmpty(password)) + throw new ArgumentException("Password cannot be null or empty", nameof(password)); + + if (salt == null || salt.Length != SaltSize) + throw new ArgumentException($"Salt must be {SaltSize} bytes", nameof(salt)); + + return Rfc2898DeriveBytes.Pbkdf2( + password, + salt, + Iterations, + HashAlgorithmName.SHA256, + KeySize); + } + + /// + /// Derive a key and convert to hex string (for SQLCipher connection string) + /// + /// User's master password + /// Random salt + /// Hex-encoded encryption key + public string DeriveKeyAsHex(string password, byte[] salt) + { + var key = DeriveKey(password, salt); + return Convert.ToHexString(key).ToLower(); + } + + /// + /// Validate password strength + /// + public (bool IsValid, string ErrorMessage) ValidatePasswordStrength(string password) + { + if (string.IsNullOrWhiteSpace(password)) + return (false, "Password cannot be empty"); + + if (password.Length < 12) + return (false, "Password must be at least 12 characters long"); + + bool hasUpper = password.Any(char.IsUpper); + bool hasLower = password.Any(char.IsLower); + bool hasDigit = password.Any(char.IsDigit); + bool hasSpecial = password.Any(ch => !char.IsLetterOrDigit(ch)); + + int categories = (hasUpper ? 1 : 0) + (hasLower ? 1 : 0) + + (hasDigit ? 1 : 0) + (hasSpecial ? 1 : 0); + + if (categories < 3) + return (false, "Password must contain at least 3 of: uppercase, lowercase, numbers, special characters"); + + return (true, string.Empty); + } + + /// + /// Convert salt to base64 for storage + /// + public string SaltToString(byte[] salt) + { + return Convert.ToBase64String(salt); + } + + /// + /// Convert base64 string back to salt bytes + /// + public byte[] StringToSalt(string saltString) + { + return Convert.FromBase64String(saltString); + } +} diff --git a/2-Aquiis.Application/DependencyInjection.cs b/2-Aquiis.Application/DependencyInjection.cs index 943c938..5bfb2bd 100644 --- a/2-Aquiis.Application/DependencyInjection.cs +++ b/2-Aquiis.Application/DependencyInjection.cs @@ -2,6 +2,7 @@ using Aquiis.Application.Services.PdfGenerators; using Aquiis.Application.Services.Workflows; using Aquiis.Infrastructure; +using Aquiis.Infrastructure.Data; using Microsoft.Extensions.DependencyInjection; namespace Aquiis.Application; @@ -16,10 +17,12 @@ public static class DependencyInjection /// public static IServiceCollection AddApplication( this IServiceCollection services, - string connectionString) + string connectionString, + string? encryptionPassword = null, + SqlCipherConnectionInterceptor? interceptor = null) { - // Call Infrastructure registration internally - services.AddInfrastructure(connectionString); + // Call Infrastructure registration internally with encryption interceptor + services.AddInfrastructure(connectionString, encryptionPassword, interceptor); // Register all Application services services.AddScoped(); diff --git a/2-Aquiis.Application/Services/DatabasePasswordService.cs b/2-Aquiis.Application/Services/DatabasePasswordService.cs new file mode 100644 index 0000000..1767dd9 --- /dev/null +++ b/2-Aquiis.Application/Services/DatabasePasswordService.cs @@ -0,0 +1,101 @@ +using Aquiis.Core.Interfaces; +using Aquiis.Infrastructure.Services; +using Microsoft.Extensions.Logging; + +namespace Aquiis.Application.Services; + +/// +/// Service for detecting and handling encrypted databases on application startup. +/// Manages password retrieval from keychain or user prompt. +/// +public class DatabasePasswordService +{ + private readonly LinuxKeychainService _keychain; + private readonly ILogger _logger; + + public DatabasePasswordService( + LinuxKeychainService keychain, + ILogger logger) + { + _keychain = keychain; + _logger = logger; + } + + /// + /// Check if a database file is encrypted by attempting to open it + /// + public async Task IsDatabaseEncryptedAsync(string dbPath) + { + if (!File.Exists(dbPath)) + return false; + + try + { + // Try to open without password + using (var conn = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={dbPath}")) + { + await conn.OpenAsync(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT count(*) FROM sqlite_master;"; + await cmd.ExecuteScalarAsync(); + } + return false; // Opened successfully = not encrypted + } + } + catch (Microsoft.Data.Sqlite.SqliteException ex) + { + // SQLCipher error codes indicate encryption + if (ex.Message.Contains("file is not a database") || + ex.Message.Contains("file is encrypted") || + ex.SqliteErrorCode == 26) // SQLITE_NOTADB + { + _logger.LogInformation("Database is encrypted"); + return true; + } + + // Some other error - rethrow + throw; + } + } + + /// + /// Try to get database password from keychain + /// + public string? TryGetPasswordFromKeychain() + { + if (!OperatingSystem.IsLinux()) + return null; + + var key = _keychain.RetrieveKey(); + if (key != null) + { + _logger.LogInformation("Retrieved encryption key from keychain"); + } + return key; + } + + /// + /// Verify that a password can decrypt the database + /// + public async Task VerifyPasswordAsync(string dbPath, string passwordHex) + { + try + { + using (var conn = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={dbPath};Password={passwordHex}")) + { + await conn.OpenAsync(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT count(*) FROM sqlite_master;"; + await cmd.ExecuteScalarAsync(); + } + return true; + } + } + catch + { + return false; + } + } +} diff --git a/2-Aquiis.Application/Services/DatabaseService.cs b/2-Aquiis.Application/Services/DatabaseService.cs index 91ff1d5..e78c16e 100644 --- a/2-Aquiis.Application/Services/DatabaseService.cs +++ b/2-Aquiis.Application/Services/DatabaseService.cs @@ -85,4 +85,58 @@ public async Task GetIdentityPendingMigrationsCountAsync() var pending = await _identityContext.Database.GetPendingMigrationsAsync(); return pending.Count(); } + + /// + /// Gets the database settings (creates default if not exists) + /// + public async Task GetDatabaseSettingsAsync() + { + var settings = await _businessContext.DatabaseSettings.FirstOrDefaultAsync(); + + if (settings == null) + { + // Create default settings + settings = new Aquiis.Core.Entities.DatabaseSettings + { + DatabaseEncryptionEnabled = false, + LastModifiedOn = DateTime.UtcNow, + LastModifiedBy = "System" + }; + + _businessContext.DatabaseSettings.Add(settings); + await _businessContext.SaveChangesAsync(); + + _logger.LogInformation("Created default database settings"); + } + + return settings; + } + + /// + /// Updates database encryption status + /// + public async Task SetDatabaseEncryptionAsync(bool enabled, string modifiedBy = "System") + { + var settings = await GetDatabaseSettingsAsync(); + settings.DatabaseEncryptionEnabled = enabled; + settings.EncryptionChangedOn = DateTime.UtcNow; + settings.LastModifiedOn = DateTime.UtcNow; + settings.LastModifiedBy = modifiedBy; + + _businessContext.DatabaseSettings.Update(settings); + await _businessContext.SaveChangesAsync(); + + _logger.LogInformation("Database encryption {Status} by {ModifiedBy}", + enabled ? "enabled" : "disabled", modifiedBy); + } + + /// + /// Gets current database encryption status + /// + public async Task IsDatabaseEncryptionEnabledAsync() + { + var settings = await GetDatabaseSettingsAsync(); + return settings.DatabaseEncryptionEnabled; + } } + diff --git a/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs index 16f5e05..bcb75be 100644 --- a/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs +++ b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs @@ -7,9 +7,11 @@ using Aquiis.Core.Interfaces.Services; using Aquiis.Application; // ✅ Application facade using Aquiis.Application.Services; +using Aquiis.Infrastructure.Data; // For SqlCipherConnectionInterceptor using Aquiis.SimpleStart.Data; using Aquiis.SimpleStart.Entities; using Aquiis.SimpleStart.Services; +using Microsoft.Data.Sqlite; namespace Aquiis.SimpleStart.Extensions; @@ -18,6 +20,9 @@ namespace Aquiis.SimpleStart.Extensions; /// public static class ElectronServiceExtensions { + // Toggle for verbose logging (useful for troubleshooting encryption setup) + private const bool EnableVerboseLogging = true; + /// /// Adds all Electron-specific infrastructure services including database, identity, and path services. /// @@ -34,12 +39,48 @@ public static IServiceCollection AddElectronServices( // Get connection string using the path service (synchronous to avoid startup deadlock) var connectionString = GetElectronConnectionString(configuration); - // ✅ Register Application layer (includes Infrastructure internally) - services.AddApplication(connectionString); + // Check if database is encrypted and retrieve password if needed + var encryptionPassword = WebServiceExtensions.GetEncryptionPasswordIfNeeded(connectionString); - // Register Identity database context (SimpleStart-specific) - services.AddDbContext(options => - options.UseSqlite(connectionString)); + // Register encryption status as singleton for use during startup + services.AddSingleton(new EncryptionDetectionResult + { + IsEncrypted = !string.IsNullOrEmpty(encryptionPassword) + }); + + // CRITICAL: Create interceptor instance BEFORE any DbContext registration + // This single instance will be used by all DbContexts + SqlCipherConnectionInterceptor? interceptor = null; + if (!string.IsNullOrEmpty(encryptionPassword)) + { + interceptor = new SqlCipherConnectionInterceptor(encryptionPassword); + + // Clear connection pools to ensure no connections bypass the interceptor + SqliteConnection.ClearAllPools(); + if (EnableVerboseLogging) + Console.WriteLine("[ElectronServiceExtensions] Encryption interceptor created and connection pools cleared"); + } + + // ✅ Register Application layer (includes Infrastructure internally) with encryption interceptor + services.AddApplication(connectionString, encryptionPassword, interceptor); + + // Register Identity database context (SimpleStart-specific) with encryption interceptor + services.AddDbContext((serviceProvider, options) => + { + options.UseSqlite(connectionString); + if (interceptor != null) + { + options.AddInterceptors(interceptor); + } + }); + + // CRITICAL: Clear connection pools again after DbContext registration + if (!string.IsNullOrEmpty(encryptionPassword)) + { + SqliteConnection.ClearAllPools(); + if (EnableVerboseLogging) + Console.WriteLine("[ElectronServiceExtensions] Connection pools cleared after DbContext registration"); + } // Register DatabaseService now that both contexts are available services.AddScoped(sp => diff --git a/4-Aquiis.SimpleStart/Extensions/SecurityHeadersMiddleware.cs b/4-Aquiis.SimpleStart/Extensions/SecurityHeadersMiddleware.cs new file mode 100644 index 0000000..9e94000 --- /dev/null +++ b/4-Aquiis.SimpleStart/Extensions/SecurityHeadersMiddleware.cs @@ -0,0 +1,127 @@ +using Microsoft.Extensions.Primitives; + +namespace Aquiis.SimpleStart.Extensions; + +/// +/// Middleware that adds security headers including Content Security Policy (CSP) +/// +public class SecurityHeadersMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly bool _isElectron; + + public SecurityHeadersMiddleware( + RequestDelegate next, + ILogger logger, + IWebHostEnvironment environment) + { + _next = next; + _logger = logger; + _isElectron = ElectronNET.API.HybridSupport.IsElectronActive; + } + + public async Task InvokeAsync(HttpContext context) + { + // Skip CSP for Electron (desktop app doesn't need browser security restrictions) + if (!_isElectron) + { + AddSecurityHeaders(context); + } + + await _next(context); + } + + private void AddSecurityHeaders(HttpContext context) + { + var headers = context.Response.Headers; + + // Content Security Policy (CSP) + // Note: Blazor Server requires 'unsafe-inline' and 'unsafe-eval' for scripts + var csp = new List + { + "default-src 'self'", + + // Scripts: Allow self, inline scripts (Blazor requirement), and eval (Blazor SignalR requirement) + "script-src 'self' 'unsafe-inline' 'unsafe-eval'", + + // Styles: Allow self, inline styles (Bootstrap/Blazor requirement), and Bootstrap CDN + "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net", + + // Fonts: Allow self, Bootstrap Icons CDN, and data URIs + "font-src 'self' https://cdn.jsdelivr.net data:", + + // Images: Allow self, data URIs (for inline images), and blob URIs (for PDF viewer) + "img-src 'self' data: blob:", + + // Connect: Allow self for Blazor SignalR WebSocket connections + "connect-src 'self'", + + // Frames: Prevent clickjacking by not allowing this site to be framed + "frame-ancestors 'none'", + + // Base: Restrict base tag to prevent base tag hijacking + "base-uri 'self'", + + // Forms: Only allow form submissions to same origin + "form-action 'self'", + + // Objects: Block all plugins (Flash, Java, etc.) + "object-src 'none'", + + // Media: Allow self for audio/video + "media-src 'self'", + + // Workers: Allow self for web workers + "worker-src 'self' blob:", + + // Child frames: Don't allow embedding iframes + "child-src 'none'", + + // Manifests: Allow self for PWA manifest + "manifest-src 'self'" + }; + + var cspHeader = string.Join("; ", csp); + headers["Content-Security-Policy"] = new StringValues(cspHeader); + + // X-Content-Type-Options: Prevent MIME type sniffing + headers["X-Content-Type-Options"] = new StringValues("nosniff"); + + // X-Frame-Options: Prevent clickjacking (defense in depth with CSP frame-ancestors) + headers["X-Frame-Options"] = new StringValues("DENY"); + + // X-XSS-Protection: Enable browser XSS filtering (legacy browsers) + headers["X-XSS-Protection"] = new StringValues("1; mode=block"); + + // Referrer-Policy: Control referrer information + headers["Referrer-Policy"] = new StringValues("strict-origin-when-cross-origin"); + + // Permissions-Policy: Restrict browser features + var permissionsPolicy = new List + { + "accelerometer=()", + "camera=()", + "geolocation=()", + "gyroscope=()", + "magnetometer=()", + "microphone=()", + "payment=()", + "usb=()" + }; + headers["Permissions-Policy"] = new StringValues(string.Join(", ", permissionsPolicy)); + + _logger.LogDebug("Security headers added to response"); + } +} + +/// +/// Extension methods for registering SecurityHeadersMiddleware +/// +public static class SecurityHeadersMiddlewareExtensions +{ + public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} diff --git a/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs index bd010ce..8b4fc3a 100644 --- a/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs +++ b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs @@ -7,9 +7,12 @@ using Aquiis.Core.Interfaces.Services; using Aquiis.Application; // ✅ Application facade using Aquiis.Application.Services; +using Aquiis.Infrastructure.Data; // For SqlCipherConnectionInterceptor using Aquiis.SimpleStart.Data; using Aquiis.SimpleStart.Entities; using Aquiis.SimpleStart.Services; +using Aquiis.Infrastructure.Services; +using Microsoft.Data.Sqlite; namespace Aquiis.SimpleStart.Extensions; @@ -18,6 +21,9 @@ namespace Aquiis.SimpleStart.Extensions; /// public static class WebServiceExtensions { + // Toggle for verbose logging (useful for troubleshooting encryption setup) + private const bool EnableVerboseLogging = true; + /// /// Adds all Web-specific infrastructure services including database and identity. /// @@ -39,12 +45,48 @@ public static IServiceCollection AddWebServices( "Connection string not found. " + "Set DATABASE_CONNECTION_STRING environment variable or configure DefaultConnection in appsettings.json"); - // ✅ Register Application layer (includes Infrastructure internally) - services.AddApplication(connectionString); + // Check if database is encrypted and retrieve password if needed + var encryptionPassword = GetEncryptionPasswordIfNeeded(connectionString); - // Register Identity database context (SimpleStart-specific) - services.AddDbContext(options => - options.UseSqlite(connectionString)); + // Register encryption status as singleton for use during startup + services.AddSingleton(new EncryptionDetectionResult + { + IsEncrypted = !string.IsNullOrEmpty(encryptionPassword) + }); + + // CRITICAL: Create interceptor instance BEFORE any DbContext registration + // This single instance will be used by all DbContexts + SqlCipherConnectionInterceptor? interceptor = null; + if (!string.IsNullOrEmpty(encryptionPassword)) + { + interceptor = new SqlCipherConnectionInterceptor(encryptionPassword); + + // Clear connection pools to ensure no connections bypass the interceptor + SqliteConnection.ClearAllPools(); + if (EnableVerboseLogging) + Console.WriteLine("Encryption interceptor created and connection pools cleared"); + } + + // ✅ Register Application layer (includes Infrastructure internally) with encryption interceptor + services.AddApplication(connectionString, encryptionPassword, interceptor); + + // Register Identity database context (SimpleStart-specific) with encryption interceptor + services.AddDbContext((serviceProvider, options) => + { + options.UseSqlite(connectionString); + if (interceptor != null) + { + options.AddInterceptors(interceptor); + } + }); + + // CRITICAL: Clear connection pools again after DbContext registration + if (!string.IsNullOrEmpty(encryptionPassword)) + { + SqliteConnection.ClearAllPools(); + if (EnableVerboseLogging) + Console.WriteLine("Connection pools cleared after DbContext registration"); + } // Register DatabaseService now that both contexts are available services.AddScoped(sp => @@ -81,4 +123,83 @@ public static IServiceCollection AddWebServices( return services; } + + /// + /// Detects if database is encrypted and retrieves password from keychain if needed + /// + /// Encryption password, or null if database is not encrypted + internal static string? GetEncryptionPasswordIfNeeded(string connectionString) + { + try + { + // Extract database path from connection string + var builder = new SqliteConnectionStringBuilder(connectionString); + var dbPath = builder.DataSource; + + if (!File.Exists(dbPath)) + { + // Database doesn't exist yet, not encrypted + return null; + } + + // Try to open as plaintext + try + { + using (var conn = new SqliteConnection(connectionString)) + { + conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master;"; + cmd.ExecuteScalar(); + } + } + // Success - database is not encrypted + return null; + } + catch (SqliteException ex) when (ex.SqliteErrorCode == 26) // "file is not a database" + { + // Database is encrypted - try to get password from keychain + if (EnableVerboseLogging) + Console.WriteLine("Detected encrypted database, retrieving password from keychain..."); + var keychain = new LinuxKeychainService(); + var password = keychain.RetrieveKey(); + + if (string.IsNullOrEmpty(password)) + { + throw new InvalidOperationException( + "Database is encrypted but encryption password not found in keychain. " + + "Please restore from an unencrypted backup."); + } + + if (EnableVerboseLogging) + Console.WriteLine($"Encryption password retrieved successfully (length: {password.Length} chars)"); + + // CRITICAL: Clear connection pool to prevent reuse of unencrypted connections + SqliteConnection.ClearAllPools(); + if (EnableVerboseLogging) + Console.WriteLine("Connection pool cleared to force encryption on all new connections"); + + return password; + } + } + catch (InvalidOperationException) + { + throw; // Re-throw our custom messages + } + catch (Exception ex) + { + // Log but don't fail - assume database is not encrypted + Console.WriteLine($"Warning: Could not check database encryption status: {ex.Message}"); + return null; + } + } +} + +/// +/// Tracks whether database encryption was detected during startup +/// +public class EncryptionDetectionResult +{ + public bool IsEncrypted { get; set; } } diff --git a/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabaseSettings.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabaseSettings.razor index 905581c..26fb78b 100644 --- a/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabaseSettings.razor +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabaseSettings.razor @@ -4,6 +4,8 @@ @using Aquiis.Application.Services.PdfGenerators @using Aquiis.Core.Constants @using Aquiis.Core.Interfaces +@using Aquiis.Core.Interfaces.Services +@using Aquiis.Infrastructure.Services @using Microsoft.AspNetCore.Authorization @using Microsoft.Extensions.Options @using ElectronNET.API @@ -14,6 +16,10 @@ @inject IOptions AppSettings @inject IJSRuntime JSRuntime @inject IHostApplicationLifetime AppLifetime +@inject IDatabaseService DatabaseService +@inject DatabaseEncryptionService EncryptionService +@inject PasswordDerivationService PasswordDerivation +@inject UserContextService UserContext @attribute [OrganizationAuthorize("Owner", "Administrator")] @rendermode InteractiveServer @@ -55,6 +61,79 @@
} + +
+
+
+
+
Database Encryption
+
+
+
+ + +
+ + @if (isEncryptionEnabled) + { +
+ + Encryption Active - Database is encrypted at rest +
+ +
+ + +
+ } + else + { +
+ + Encryption Disabled - Database is stored in plaintext +
+

+ + What happens when you enable encryption: +

+
    +
  • You'll create a master password to encrypt the database
  • +
  • Password is cached in your system keychain for convenience
  • +
  • Database remains portable - use the same password on any device
  • +
  • A backup will be created before encryption begins
  • +
+ } +
+
+
+
+
@@ -270,7 +349,9 @@
Important Information
    -
  • Automatic Backups: Created before each migration
  • +
  • Database Encryption: Protects your data at rest using password-based encryption (Phase 2 feature)
  • +
  • Password Portability: Your password works on any device - database remains portable
  • +
  • Automatic Backups: Created before each migration and encryption change
  • Health Check: Validates database integrity using SQLite's built-in PRAGMA integrity_check
  • Auto-Recovery: Attempts to restore from the most recent valid backup
  • Retention: Last 10 backups are kept automatically, older ones are deleted
  • @@ -281,6 +362,9 @@
+@* Password input modal for Electron compatibility *@ + + @code { private List backups = new(); private string? successMessage; @@ -297,6 +381,14 @@ private bool isRestarting = false; private (bool IsHealthy, string Message)? healthCheckResult; private DateTime? lastHealthCheck; + + // Database encryption + private bool isEncryptionEnabled = false; + private bool isChangingPassword = false; + private bool isExporting = false; + + // Password modal reference + private PasswordInputModal passwordModal = null!; private async Task ResetDatabase() { @@ -412,6 +504,7 @@ { await LoadBackups(); await CheckDatabaseHealth(); + await CheckEncryptionStatus(); } private async Task LoadBackups() @@ -734,4 +827,366 @@ } return $"{len:0.##} {sizes[order]}"; } + + // Database Encryption Methods + + private async Task CheckEncryptionStatus() + { + try + { + // Load encryption status from database settings + isEncryptionEnabled = await DatabaseService.IsDatabaseEncryptionEnabledAsync(); + } + catch (Exception ex) + { + // Log to server console (not JavaScript) since this runs during prerender + Console.WriteLine($"Error checking encryption status: {ex.Message}"); + isEncryptionEnabled = false; + } + } + + private async Task ToggleEncryption(ChangeEventArgs e) + { + bool enabled = (bool)e.Value!; + + if (enabled) + { + // Enable encryption - prompt for password with confirmation + var password = await passwordModal.ShowAsync( + "Create Master Password", + "Create a master password for database encryption.\n\n" + + "IMPORTANT: This password cannot be recovered if lost!\n" + + "Minimum 12 characters required.", + requireConfirmation: true); + + if (string.IsNullOrWhiteSpace(password)) + { + errorMessage = "Encryption cancelled - no password provided."; + isEncryptionEnabled = false; // Reset toggle to OFF + StateHasChanged(); + return; + } + + var confirmed = await JSRuntime.InvokeAsync("confirm", + "⚠️ IMPORTANT WARNINGS ⚠️\n\n" + + "• A backup will be created before encryption\n" + + "• Application will restart after encryption\n" + + "• You'll need this password to access the database\n" + + "• Lost password = Lost data (no recovery possible)\n\n" + + "Proceed with encryption?"); + + if (!confirmed) + { + errorMessage = "Encryption cancelled by user."; + isEncryptionEnabled = false; // Reset toggle to OFF + StateHasChanged(); + return; + } + + await EnableEncryption(password); + } + else + { + // Disable encryption - prompt for password to decrypt + var password = await passwordModal.ShowAsync( + "Decrypt Database", + "Enter your master password to decrypt the database:", + requireConfirmation: false); + + if (string.IsNullOrWhiteSpace(password)) + { + errorMessage = "Decryption cancelled - no password provided."; + isEncryptionEnabled = true; // Keep it enabled since decrypt was cancelled + StateHasChanged(); + return; + } + + var confirmed = await JSRuntime.InvokeAsync("confirm", + "⚠️ WARNING ⚠️\n\n" + + "This will decrypt your database and store it in plaintext.\n" + + "A backup will be created before decryption.\n\n" + + "Proceed with decryption?"); + + if (!confirmed) + { + errorMessage = "Decryption cancelled by user."; + isEncryptionEnabled = true; // Keep it enabled since decrypt was cancelled + StateHasChanged(); + return; + } + + await DisableEncryption(password); + } + } + + private async Task EnableEncryption(string password) + { + try + { + errorMessage = null; + successMessage = "Creating backup before encryption..."; + StateHasChanged(); + + // Step 1: Create backup + var backupPath = await BackupService.CreateBackupAsync("BeforeEncryption"); + + if (string.IsNullOrEmpty(backupPath)) + { + errorMessage = "Failed to create backup. Encryption cancelled."; + isEncryptionEnabled = false; + StateHasChanged(); + return; + } + + successMessage = "Encrypting database... This may take a moment."; + StateHasChanged(); + + // Step 2: Get database path and close connections + var dbPath = await PathService.GetDatabasePathAsync(); + await BackupService.PrepareForFileCopyAsync(); + + // Step 3: Copy database file + var copyPath = $"{dbPath}.copy_to_encrypt"; + File.Copy(dbPath, copyPath, overwrite: true); + + // Step 4: Encrypt the copy (with raw password) + var (success, encryptedPath, errorMsg) = + await EncryptionService.EncryptDatabaseAsync(copyPath, password); + + if (!success || encryptedPath == null) + { + errorMessage = $"Encryption failed: {errorMsg}"; + isEncryptionEnabled = false; + if (File.Exists(copyPath)) File.Delete(copyPath); + if (File.Exists($"{copyPath}-wal")) File.Delete($"{copyPath}-wal"); + if (File.Exists($"{copyPath}-shm")) File.Delete($"{copyPath}-shm"); + if (encryptedPath != null && File.Exists(encryptedPath)) File.Delete(encryptedPath); + StateHasChanged(); + return; + } + + // Step 5: Stage encrypted db for restart + var stagedRestorePath = $"{dbPath}.restore_pending"; + File.Move(encryptedPath, stagedRestorePath, overwrite: true); + + // Cleanup temp copy and SQLite auxiliary files + if (File.Exists(copyPath)) File.Delete(copyPath); + if (File.Exists($"{copyPath}-wal")) File.Delete($"{copyPath}-wal"); + if (File.Exists($"{copyPath}-shm")) File.Delete($"{copyPath}-shm"); + + successMessage = "✅ Database encrypted successfully! Application will restart..."; + StateHasChanged(); + await Task.Delay(2000); + + // Step 6: Restart - Program.cs will swap in .restore_pending + if (HybridSupport.IsElectronActive) + { + Electron.App.Relaunch(); + Electron.App.Exit(); + } + else + { + AppLifetime.StopApplication(); + } + } + catch (Exception ex) + { + errorMessage = $"Error enabling encryption: {ex.Message}"; + isEncryptionEnabled = false; + StateHasChanged(); + } + } + + private async Task DisableEncryption(string password) + { + try + { + errorMessage = null; + successMessage = "Creating backup before decryption..."; + StateHasChanged(); + + // Create backup before decryption + var backupPath = await BackupService.CreateBackupAsync("BeforeDecryption"); + + if (string.IsNullOrEmpty(backupPath)) + { + errorMessage = "Failed to create backup. Decryption cancelled."; + isEncryptionEnabled = true; + StateHasChanged(); + return; + } + + successMessage = "Decrypting database copy... This may take a moment."; + StateHasChanged(); + + // Get database path + var dbPath = await PathService.GetDatabasePathAsync(); + + // CRITICAL: Close connections and prepare for file copy + await BackupService.PrepareForFileCopyAsync(); + + successMessage = "Copying encrypted database... This may take a moment."; + StateHasChanged(); + + // Create a temporary copy to decrypt (can't decrypt live database - it's locked) + var tempCopyPath = $"{dbPath}.temp_for_decryption"; + File.Copy(dbPath, tempCopyPath, overwrite: true); + + try + { + // Decrypt the copy (not the live database) using raw password + var (success, decryptedPath, errorMsg) = + await EncryptionService.DecryptDatabaseAsync(tempCopyPath, password); + + if (!success || decryptedPath == null) + { + errorMessage = $"Decryption failed: {errorMsg ?? "Invalid password or corrupted database"}"; + isEncryptionEnabled = true; + StateHasChanged(); + + // Cleanup temp files and SQLite auxiliary files + if (File.Exists(tempCopyPath)) File.Delete(tempCopyPath); + if (File.Exists($"{tempCopyPath}-wal")) File.Delete($"{tempCopyPath}-wal"); + if (File.Exists($"{tempCopyPath}-shm")) File.Delete($"{tempCopyPath}-shm"); + if (decryptedPath != null && File.Exists(decryptedPath)) File.Delete(decryptedPath); + return; + } + + // Stage decrypted database for restart (same pattern as restore operations) + var stagedRestorePath = $"{dbPath}.restore_pending"; + File.Move(decryptedPath, stagedRestorePath, overwrite: true); + + // Cleanup temp copy and SQLite auxiliary files + if (File.Exists(tempCopyPath)) File.Delete(tempCopyPath); + if (File.Exists($"{tempCopyPath}-wal")) File.Delete($"{tempCopyPath}-wal"); + if (File.Exists($"{tempCopyPath}-shm")) File.Delete($"{tempCopyPath}-shm"); + + successMessage = "✅ Database decrypted successfully! Application will restart..."; + StateHasChanged(); + await Task.Delay(2000); + + // Restart the application - on startup it will swap in the decrypted database + if (HybridSupport.IsElectronActive) + { + Electron.App.Relaunch(); + Electron.App.Exit(); + } + else + { + AppLifetime.StopApplication(); + } + } + catch (Exception ex) + { + errorMessage = $"Decryption error: {ex.Message}"; + isEncryptionEnabled = true; + StateHasChanged(); + + // Cleanup on error + if (File.Exists(tempCopyPath)) File.Delete(tempCopyPath); + } + } + catch (Exception ex) + { + errorMessage = $"Error disabling encryption: {ex.Message}"; + isEncryptionEnabled = true; + StateHasChanged(); + } + } + + private async Task ChangeEncryptionPassword() + { + isChangingPassword = true; + errorMessage = null; + + try + { + var currentPassword = await passwordModal.ShowAsync( + "Current Password", + "Enter your current master password:", + requireConfirmation: false); + + if (string.IsNullOrWhiteSpace(currentPassword)) + { + errorMessage = "Password change cancelled."; + return; + } + + var newPassword = await passwordModal.ShowAsync( + "New Master Password", + "Enter new master password (minimum 12 characters):", + requireConfirmation: true); + + if (string.IsNullOrWhiteSpace(newPassword)) + { + errorMessage = "Password change cancelled."; + return; + } + + // TODO: Implement password change + // 1. Verify current password + // 2. Re-encrypt database with new password-derived key + // 3. Update OS keychain with new key + // 4. Restart application + + errorMessage = "⚠️ Password change feature not yet implemented (Phase 2)."; + } + catch (Exception ex) + { + errorMessage = $"Error changing password: {ex.Message}"; + } + finally + { + isChangingPassword = false; + } + } + + private async Task ExportUnencryptedBackup() + { + isExporting = true; + errorMessage = null; + + try + { + var password = await passwordModal.ShowAsync( + "Export Unencrypted Backup", + "Enter your master password to export unencrypted backup:", + requireConfirmation: false); + + if (string.IsNullOrWhiteSpace(password)) + { + errorMessage = "Export cancelled."; + return; + } + + var confirmed = await JSRuntime.InvokeAsync("confirm", + "⚠️ SECURITY WARNING ⚠️\n\n" + + "This will create an UNENCRYPTED backup of your database.\n" + + "Anyone with access to this file can read all your data.\n\n" + + "Only use this if you need to transfer data to another system.\n\n" + + "Continue?"); + + if (!confirmed) + { + errorMessage = "Export cancelled."; + return; + } + + // TODO: Implement unencrypted export + // 1. Verify password + // 2. Decrypt database to temporary file + // 3. Trigger download + // 4. Delete temporary file + + errorMessage = "⚠️ Unencrypted export feature not yet implemented (Phase 2)."; + } + catch (Exception ex) + { + errorMessage = $"Error exporting backup: {ex.Message}"; + } + finally + { + isExporting = false; + } + } } diff --git a/4-Aquiis.SimpleStart/Program.cs b/4-Aquiis.SimpleStart/Program.cs index e575e41..2bba786 100644 --- a/4-Aquiis.SimpleStart/Program.cs +++ b/4-Aquiis.SimpleStart/Program.cs @@ -3,11 +3,12 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Aquiis.Core.Constants; +using Aquiis.Core.Entities; using Aquiis.Core.Interfaces; using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Extensions; using Aquiis.SimpleStart.Shared.Services; using Aquiis.SimpleStart.Shared.Authorization; -using Aquiis.SimpleStart.Extensions; using Aquiis.Application.Services; using Aquiis.Application.Services.Workflows; using Aquiis.SimpleStart.Data; @@ -16,10 +17,18 @@ using Microsoft.Extensions.Options; using Aquiis.Application.Services.PdfGenerators; using Aquiis.SimpleStart.Shared.Components.Account; +using Aquiis.Infrastructure.Services; +// Initialize SQLCipher before any database operations +SQLitePCL.Batteries_V2.Init(); +SQLitePCL.raw.sqlite3_initialize(); var builder = WebApplication.CreateBuilder(args); +// CRITICAL: Handle .restore_pending BEFORE any DbContext registration +// This ensures encrypted database detection happens on the correct file +HandlePendingRestore(builder.Configuration); + // Configure for Electron builder.WebHost.UseElectron(args); @@ -169,6 +178,12 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Database encryption services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Configure and register session timeout service builder.Services.AddScoped(sp => { @@ -393,6 +408,25 @@ // Create initial backup after database creation await backupService.CreateBackupAsync("InitialSetup"); } + + // Update DatabaseSettings.DatabaseEncryptionEnabled flag to match actual encryption status + var encryptionDetection = scope.ServiceProvider.GetRequiredService(); + var currentSettings = await dbService.GetDatabaseSettingsAsync(); + + if (currentSettings.DatabaseEncryptionEnabled != encryptionDetection.IsEncrypted) + { + app.Logger.LogInformation( + "Updating DatabaseSettings.DatabaseEncryptionEnabled from {Old} to {New} (detected actual status)", + currentSettings.DatabaseEncryptionEnabled, + encryptionDetection.IsEncrypted); + await dbService.SetDatabaseEncryptionAsync(encryptionDetection.IsEncrypted, "System-AutoDetect"); + } + else + { + app.Logger.LogInformation( + "DatabaseSettings.DatabaseEncryptionEnabled already matches actual encryption status: {Status}", + encryptionDetection.IsEncrypted); + } } catch (Exception ex) { @@ -408,47 +442,9 @@ app.Logger.LogInformation("Applying database migrations for web mode"); // Get database path for web mode - var webConnectionString = builder.Configuration.GetConnectionString("DefaultConnection"); - if (!string.IsNullOrEmpty(webConnectionString)) - { - var dbPath = webConnectionString - .Replace("Data Source=", "") - .Replace("DataSource=", "") - .Split(';')[0] - .Trim(); - - if (!Path.IsPathRooted(dbPath)) - { - dbPath = Path.Combine(Directory.GetCurrentDirectory(), dbPath); - } - - var stagedRestorePath = $"{dbPath}.restore_pending"; - - // Check if there's a staged restore waiting - if (File.Exists(stagedRestorePath)) - { - app.Logger.LogInformation("Found staged restore file for web mode, applying it now"); - - // Clear SQLite connection pool - Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); - - // Wait for connections to close - await Task.Delay(500); - - // Backup current database if it exists - if (File.Exists(dbPath)) - { - var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff"); - var beforeRestorePath = $"{dbPath}.beforeRestore.{timestamp}"; - File.Move(dbPath, beforeRestorePath); - app.Logger.LogInformation("Current database backed up to: {Path}", beforeRestorePath); - } - - // Move staged restore into place - File.Move(stagedRestorePath, dbPath); - app.Logger.LogInformation("Staged restore applied successfully for web mode"); - } - } + // REMOVED: .restore_pending handling now happens BEFORE service registration + // This ensures encrypted database detection works correctly + // See HandlePendingRestore() called before AddWebServices() // Check if there are pending migrations for both contexts var businessPendingCount = await dbService.GetPendingMigrationsCountAsync(); @@ -480,6 +476,25 @@ app.Logger.LogInformation("Database migrations applied successfully"); + // Update DatabaseSettings.DatabaseEncryptionEnabled flag to match actual encryption status + var encryptionDetection = scope.ServiceProvider.GetRequiredService(); + var currentSettings = await dbService.GetDatabaseSettingsAsync(); + + if (currentSettings.DatabaseEncryptionEnabled != encryptionDetection.IsEncrypted) + { + app.Logger.LogInformation( + "Updating DatabaseSettings.DatabaseEncryptionEnabled from {Old} to {New} (detected actual status)", + currentSettings.DatabaseEncryptionEnabled, + encryptionDetection.IsEncrypted); + await dbService.SetDatabaseEncryptionAsync(encryptionDetection.IsEncrypted, "System-AutoDetect"); + } + else + { + app.Logger.LogInformation( + "DatabaseSettings.DatabaseEncryptionEnabled already matches actual encryption status: {Status}", + encryptionDetection.IsEncrypted); + } + // Create initial backup after creating a new database if (isNewDatabase) { @@ -535,6 +550,9 @@ app.UseSession(); +// ✅ SECURITY: Content Security Policy and security headers +app.UseSecurityHeaders(); + // ✅ SECURITY: HTTPS enforcement for production web mode if (!HybridSupport.IsElectronActive) { @@ -590,6 +608,9 @@ }).RequireAuthorization(); // Create system service account for background jobs +// Clear connection pool to ensure all connections use proper encryption interceptor +Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); + using (var scope = app.Services.CreateScope()) { var userManager = scope.ServiceProvider.GetRequiredService>(); @@ -699,3 +720,56 @@ } await app.WaitForShutdownAsync(); + +// Local function to handle .restore_pending before service registration +static void HandlePendingRestore(IConfiguration configuration) +{ + var connectionString = configuration.GetConnectionString("DefaultConnection"); + if (string.IsNullOrEmpty(connectionString)) + { + // Can't proceed without connection string + return; + } + + // Extract database path + var builder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder(connectionString); + var dbPath = builder.DataSource; + + if (!Path.IsPathRooted(dbPath)) + { + dbPath = Path.Combine(Directory.GetCurrentDirectory(), dbPath); + } + + var stagedRestorePath = $"{dbPath}.restore_pending"; + + // Check if there's a staged restore waiting + if (File.Exists(stagedRestorePath)) + { + Console.WriteLine("Found staged restore file, applying it now..."); + + // Clear SQLite connection pool + Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); + + // Wait for connections to close + Thread.Sleep(500); + + // Backup current database if it exists + if (File.Exists(dbPath)) + { + var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff"); + var beforeRestorePath = $"{dbPath}.beforeRestore.{timestamp}"; + File.Move(dbPath, beforeRestorePath); + Console.WriteLine($"Current database backed up to: {beforeRestorePath}"); + } + + // Move staged restore into place + File.Move(stagedRestorePath, dbPath); + Console.WriteLine("Staged restore applied successfully"); + + // Delete orphaned WAL/SHM files if they exist + var walPath = $"{dbPath}-wal"; + var shmPath = $"{dbPath}-shm"; + if (File.Exists(walPath)) File.Delete(walPath); + if (File.Exists(shmPath)) File.Delete(shmPath); + } +} diff --git a/4-Aquiis.SimpleStart/Shared/Components/PasswordInputModal.razor b/4-Aquiis.SimpleStart/Shared/Components/PasswordInputModal.razor new file mode 100644 index 0000000..62552ea --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/PasswordInputModal.razor @@ -0,0 +1,226 @@ +@* Pure Blazor modal - no JavaScript required, works in Electron *@ +@if (isVisible) +{ + + +} + +@code { + private string? password; + private string? confirmPassword; + private bool showPassword = false; + private bool showConfirmPassword = false; + private string? validationError; + private bool isVisible = false; + private TaskCompletionSource? taskCompletionSource; + + [Parameter] public int MinPasswordLength { get; set; } = 12; + [Parameter] public bool ShowSecurityNote { get; set; } = true; + + // Properties set when showing modal + private string Title { get; set; } = "Enter Password"; + private string? Message { get; set; } + private bool RequireConfirmation { get; set; } + + /// + /// Shows the password modal and waits for user input + /// + /// Modal title + /// Optional message to display + /// If true, requires password confirmation + /// The entered password, or null if cancelled + public async Task ShowAsync(string title, string? message = null, bool requireConfirmation = false) + { + // Reset state + Title = title; + Message = message; + RequireConfirmation = requireConfirmation; + password = null; + confirmPassword = null; + showPassword = false; + showConfirmPassword = false; + validationError = null; + + taskCompletionSource = new TaskCompletionSource(); + + // Show modal using Blazor state + isVisible = true; + StateHasChanged(); + + // Wait for user to submit or cancel + return await taskCompletionSource.Task; + } + + private bool IsValid() + { + if (string.IsNullOrWhiteSpace(password)) + return false; + + if (password.Length < MinPasswordLength) + return false; + + if (RequireConfirmation && password != confirmPassword) + return false; + + return true; + } + + private void TogglePasswordVisibility() + { + showPassword = !showPassword; + } + + private void ToggleConfirmPasswordVisibility() + { + showConfirmPassword = !showConfirmPassword; + } + + private void HandleKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter" && IsValid()) + { + _ = HandleSubmit(); + } + else if (e.Key == "Escape") + { + _ = HandleCancel(); + } + } + + private async Task HandleSubmit() + { + validationError = null; + + if (string.IsNullOrWhiteSpace(password)) + { + validationError = "Password is required"; + return; + } + + if (password.Length < MinPasswordLength) + { + validationError = $"Password must be at least {MinPasswordLength} characters"; + return; + } + + if (RequireConfirmation && password != confirmPassword) + { + validationError = "Passwords do not match"; + return; + } + + // Hide modal + isVisible = false; + StateHasChanged(); + + // Small delay to allow modal to close smoothly + await Task.Delay(100); + + // Return password + taskCompletionSource?.SetResult(password); + } + + private async Task HandleCancel() + { + // Hide modal + isVisible = false; + StateHasChanged(); + + // Small delay to allow modal to close smoothly + await Task.Delay(100); + + // Return null (cancelled) + taskCompletionSource?.SetResult(null); + } +} diff --git a/4-Aquiis.SimpleStart/Shared/Services/DatabaseBackupService.cs b/4-Aquiis.SimpleStart/Shared/Services/DatabaseBackupService.cs index 610d889..8a16260 100644 --- a/4-Aquiis.SimpleStart/Shared/Services/DatabaseBackupService.cs +++ b/4-Aquiis.SimpleStart/Shared/Services/DatabaseBackupService.cs @@ -390,6 +390,71 @@ await Task.Run(() => }); } + /// + /// Prepares the database for file operations by checkpointing WAL, closing connections, and clearing pools. + /// Call this before attempting to copy or move the live database file. + /// + /// Optional connection string if database is encrypted (include password) + public async Task PrepareForFileCopyAsync(string? connectionString = null) + { + try + { + // Step 1: Checkpoint WAL to consolidate all data into main database file + _logger.LogInformation("Checkpointing WAL before file operation"); + + if (string.IsNullOrEmpty(connectionString)) + { + // Use DbContext connection for plaintext database + var connection = _dbContext.Database.GetDbConnection(); + await connection.OpenAsync(); + using (var command = connection.CreateCommand()) + { + command.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + await command.ExecuteNonQueryAsync(); + } + await connection.CloseAsync(); + } + else + { + // Use provided connection string for encrypted database + using (var connection = new Microsoft.Data.Sqlite.SqliteConnection(connectionString)) + { + await connection.OpenAsync(); + using (var command = connection.CreateCommand()) + { + command.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + await command.ExecuteNonQueryAsync(); + } + } + } + + _logger.LogInformation("WAL checkpoint completed successfully"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "WAL checkpoint failed, continuing anyway"); + } + + // Step 2: Close DbContext connection + try + { + await _dbContext.Database.CloseConnectionAsync(); + _logger.LogInformation("Database connection closed"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error closing database connection"); + } + + // Step 3: Clear all SQLite connection pools to release file locks + Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); + _logger.LogInformation("Connection pools cleared"); + + // Step 4: Wait for file system to release locks (increased from 100ms for encryption reliability) + await Task.Delay(500); + _logger.LogInformation("Database prepared for file operations"); + } + private string FormatFileSize(long bytes) { string[] sizes = { "B", "KB", "MB", "GB" }; diff --git a/5-Aquiis.Professional/Extensions/SecurityHeadersMiddleware.cs b/5-Aquiis.Professional/Extensions/SecurityHeadersMiddleware.cs new file mode 100644 index 0000000..587363f --- /dev/null +++ b/5-Aquiis.Professional/Extensions/SecurityHeadersMiddleware.cs @@ -0,0 +1,127 @@ +using Microsoft.Extensions.Primitives; + +namespace Aquiis.Professional.Extensions; + +/// +/// Middleware that adds security headers including Content Security Policy (CSP) +/// +public class SecurityHeadersMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly bool _isElectron; + + public SecurityHeadersMiddleware( + RequestDelegate next, + ILogger logger, + IWebHostEnvironment environment) + { + _next = next; + _logger = logger; + _isElectron = ElectronNET.API.HybridSupport.IsElectronActive; + } + + public async Task InvokeAsync(HttpContext context) + { + // Skip CSP for Electron (desktop app doesn't need browser security restrictions) + if (!_isElectron) + { + AddSecurityHeaders(context); + } + + await _next(context); + } + + private void AddSecurityHeaders(HttpContext context) + { + var headers = context.Response.Headers; + + // Content Security Policy (CSP) + // Note: Blazor Server requires 'unsafe-inline' and 'unsafe-eval' for scripts + var csp = new List + { + "default-src 'self'", + + // Scripts: Allow self, inline scripts (Blazor requirement), and eval (Blazor SignalR requirement) + "script-src 'self' 'unsafe-inline' 'unsafe-eval'", + + // Styles: Allow self, inline styles (Bootstrap/Blazor requirement), and Bootstrap CDN + "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net", + + // Fonts: Allow self, Bootstrap Icons CDN, and data URIs + "font-src 'self' https://cdn.jsdelivr.net data:", + + // Images: Allow self, data URIs (for inline images), and blob URIs (for PDF viewer) + "img-src 'self' data: blob:", + + // Connect: Allow self for Blazor SignalR WebSocket connections + "connect-src 'self'", + + // Frames: Prevent clickjacking by not allowing this site to be framed + "frame-ancestors 'none'", + + // Base: Restrict base tag to prevent base tag hijacking + "base-uri 'self'", + + // Forms: Only allow form submissions to same origin + "form-action 'self'", + + // Objects: Block all plugins (Flash, Java, etc.) + "object-src 'none'", + + // Media: Allow self for audio/video + "media-src 'self'", + + // Workers: Allow self for web workers + "worker-src 'self' blob:", + + // Child frames: Don't allow embedding iframes + "child-src 'none'", + + // Manifests: Allow self for PWA manifest + "manifest-src 'self'" + }; + + var cspHeader = string.Join("; ", csp); + headers["Content-Security-Policy"] = new StringValues(cspHeader); + + // X-Content-Type-Options: Prevent MIME type sniffing + headers["X-Content-Type-Options"] = new StringValues("nosniff"); + + // X-Frame-Options: Prevent clickjacking (defense in depth with CSP frame-ancestors) + headers["X-Frame-Options"] = new StringValues("DENY"); + + // X-XSS-Protection: Enable browser XSS filtering (legacy browsers) + headers["X-XSS-Protection"] = new StringValues("1; mode=block"); + + // Referrer-Policy: Control referrer information + headers["Referrer-Policy"] = new StringValues("strict-origin-when-cross-origin"); + + // Permissions-Policy: Restrict browser features + var permissionsPolicy = new List + { + "accelerometer=()", + "camera=()", + "geolocation=()", + "gyroscope=()", + "magnetometer=()", + "microphone=()", + "payment=()", + "usb=()" + }; + headers["Permissions-Policy"] = new StringValues(string.Join(", ", permissionsPolicy)); + + _logger.LogDebug("Security headers added to response"); + } +} + +/// +/// Extension methods for registering SecurityHeadersMiddleware +/// +public static class SecurityHeadersMiddlewareExtensions +{ + public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} diff --git a/5-Aquiis.Professional/Program.cs b/5-Aquiis.Professional/Program.cs index 432611f..a3ff18d 100644 --- a/5-Aquiis.Professional/Program.cs +++ b/5-Aquiis.Professional/Program.cs @@ -534,6 +534,9 @@ app.UseSession(); +// ✅ SECURITY: Content Security Policy and security headers +app.UseSecurityHeaders(); + // ✅ SECURITY: HTTPS enforcement for production web mode if (!HybridSupport.IsElectronActive) { diff --git a/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs b/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs index 0a75d07..f6baed0 100644 --- a/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs +++ b/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs @@ -39,9 +39,9 @@ public async Task CreateNewAccount() await Page.GetByRole(AriaRole.Textbox, new() { Name = "First Name" }).PressAsync("Tab"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Last Name" }).FillAsync("One"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Last Name" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Confirm Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Confirm Password" }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Button, new() { Name = "Register" }).ClickAsync(); // await Page.WaitForSelectorAsync("h1:has-text('Register confirmation')"); @@ -79,7 +79,7 @@ public async Task AddProperty() await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); // Wait for login to complete @@ -148,7 +148,7 @@ public async Task AddProspect() await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); @@ -186,7 +186,7 @@ public async Task ScheduleAndCompleteTour() await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); @@ -253,7 +253,7 @@ public async Task SubmitApplication() await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); @@ -325,7 +325,7 @@ public async Task ApproveApplication() await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); @@ -370,7 +370,7 @@ public async Task GenerateLeaseOfferAndConvertToLease() await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); diff --git a/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs b/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs index 6e84aac..e60e3e0 100644 --- a/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs +++ b/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs @@ -39,9 +39,9 @@ public async Task CreateNewAccount() await Page.GetByRole(AriaRole.Textbox, new() { Name = "First Name" }).PressAsync("Tab"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Last Name" }).FillAsync("One"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Last Name" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Confirm Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Confirm Password" }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Button, new() { Name = "Register" }).ClickAsync(); //await Page.WaitForSelectorAsync("h1:has-text('Register confirmation')"); @@ -79,7 +79,7 @@ public async Task AddProperty() await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("SamplePassword2025!"); await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); // Wait for login to complete diff --git a/Documentation/Compatibility-Matrix.md b/Documentation/Compatibility-Matrix.md new file mode 100644 index 0000000..44b05d5 --- /dev/null +++ b/Documentation/Compatibility-Matrix.md @@ -0,0 +1,258 @@ +# Aquiis Compatibility Matrix + +**Last Updated:** February 1, 2026 +**Current Release:** v1.0.1 (SimpleStart) | v0.3.0 (Professional) +**Next Release:** v1.1.0 (SimpleStart) | v0.3.1 (Professional) + +--- + +## Overview + +This matrix tracks version compatibility across Aquiis releases, enabling you to: + +- ✅ **Verify upgrade/downgrade compatibility** - Check if app versions work with your database +- ✅ **Identify component versions** - Know which dependencies are installed +- ✅ **Plan rollbacks safely** - Understand which versions can coexist +- ✅ **Troubleshoot version mismatches** - Diagnose compatibility issues +- ✅ **Track breaking changes** - See when incompatibilities were introduced + +**For detailed release information**, see version-specific Release Notes in `Documentation/vX.X.X/`. + +--- + +## SimpleStart Version History + +| Release Date | App Version | Database Schema | .NET SDK | ElectronNET | Bootstrap | QuestPDF | Migration Required | Breaking Changes | Status | Download | +| ------------ | ----------- | --------------- | -------- | ----------- | --------- | --------- | ------------------ | ---------------- | ------------------ | -------------------------------------------------------------------- | +| TBD | **1.1.0** | v1.0.0 | 10.0.1 | 23.6.2 | 5.3.3 | 2025.12.1 | Auto (location) | Database path | **In Development** | - | +| 2026-01-29 | 1.0.1 | v1.0.0 | 10.0.1 | 23.6.2 | 5.3.3 | 2025.12.1 | No | No | **Current** | [Release](https://github.com/xnodeoncode/Aquiis/releases/tag/v1.0.1) | +| 2026-01-28 | 1.0.0 | v1.0.0 | 10.0.1 | 23.6.2 | 5.3.3 | 2025.12.1 | No | No | Superseded | [Release](https://github.com/xnodeoncode/Aquiis/releases/tag/v1.0.0) | + +## Professional Version History + +| Release Date | App Version | Database Schema | .NET SDK | ElectronNET | Bootstrap | QuestPDF | Migration Required | Breaking Changes | Status | Download | +| ------------ | ----------- | --------------- | -------- | ----------- | --------- | --------- | ------------------ | ---------------- | ------------------ | -------- | +| TBD | **0.3.1** | v0.0.0 | 10.0.1 | 23.6.2 | 5.3.3 | 2025.12.1 | Auto (location) | Database path | **In Development** | - | +| 2026-01-15 | 0.3.0 | v0.0.0 | 10.0.1 | 23.6.2 | 5.3.3 | 2025.12.1 | No | No | **Current** | - | + +## Pre-Release History (Archived) + +| Release Date | App Version | Database Schema | .NET SDK | Status | +| ------------ | ----------- | --------------- | -------- | ----------- | +| 2026-01-05 | 0.2.0 | v0.0.0 | 10.0.0 | Pre-release | +| 2025-12-20 | 0.1.0 | v0.0.0 | 9.0.0 | Alpha | + +--- + +## Component Details + +### Core Framework + +| Component | Current Version | Purpose | Upgrade Notes | +| ----------------- | --------------- | ---------------- | -------------------------------- | +| **.NET SDK** | 10.0.1 | Runtime platform | Auto-included in Electron builds | +| **ASP.NET Core** | 10.0.1 | Web framework | Included with .NET SDK | +| **Blazor Server** | 10.0.1 | UI framework | Included with ASP.NET Core | + +### Desktop Integration + +| Component | Current Version | Purpose | Upgrade Notes | +| -------------------- | --------------- | ----------------- | ---------------------------------------------------------------- | +| **ElectronNET.API** | 23.6.2 | Desktop framework | Major version changes may require electron.manifest.json updates | +| **electron-builder** | 26.4.0 | Build/packaging | Controls AppImage/exe generation | + +### Database & Storage + +| Component | Current Version | Purpose | Upgrade Notes | +| ------------------- | -------------------- | --------------- | ---------------------------------------------- | +| **SQLite** | 3.46.0 | Database engine | Via Microsoft.Data.Sqlite | +| **EF Core** | 10.0.1 | ORM | Breaking changes uncommon in minor versions | +| **Database Schema** | v1.0.0 (SimpleStart) | Data structure | Tracks with app version MAJOR.MINOR milestones | +| | v0.0.0 (Professional)| Data structure | Pre-v1.0.0 rapid iteration phase | + +### UI & Front-end + +| Component | Current Version | Purpose | Upgrade Notes | +| ------------------------- | --------------- | ------------- | ----------------------------- | +| **Bootstrap** | 5.3.3 | CSS framework | Generally backward compatible | +| **Bootstrap Icons** | 1.11.3 | Icon font | Additive changes only | +| **Material Design Icons** | 7.4.47 | Icon font | Additive changes only | + +### Document Generation + +| Component | Current Version | Purpose | Upgrade Notes | +| ------------ | --------------- | -------------- | -------------------------------------------------- | +| **QuestPDF** | 2025.12.1 | PDF generation | Annual major versions, breaking changes documented | + +### External Services (Optional) + +| Component | Current Version | Purpose | Upgrade Notes | +| ------------ | --------------- | -------------- | -------------------------------------------------- | +| **SendGrid** | 9.29.3 | Email delivery | API key required for email notifications | +| **Twilio** | 7.14.0 | SMS delivery | Account credentials required for SMS notifications | + +--- + +## Database Schema Versioning + +**Current Schema:** +- **SimpleStart:** v1.0.0 (production) +- **Professional:** v0.0.0 (pre-release) + +### Schema Version Strategy + +- **v1.0.0** (SimpleStart): Initial production schema + - Entity models stabilized for production + - Schema managed via EF Core Migrations + - Database filename: `app_v1.0.0.db` + +- **v0.0.0** (Professional): Pre-v1.0.0 rapid iteration + - Schema changes without version increments + - Allows fast development iterations + - Database filename: `app_v0.0.0.db` + +- **Future versions**: + - **MAJOR** (vX.0.0): Breaking schema changes requiring migration + - Database filename updates to `app_vX.0.0.db` + - Automatic backup created before migration + - **MINOR** (v1.X.0): New tables/columns, backward compatible + - Database filename may update to `app_v1.X.0.db` if schema changes + - **PATCH** (v1.0.X): No schema changes + - Database filename remains unchanged + +--- + +## Version Compatibility + +### Rollback Safety + +| From Version | To Version | Database Compatible | Safe Rollback | Notes | +| ------------ | ---------- | ------------------- | ------------- | ---------------------------------------- | +| v1.1.0 | v1.0.1 | ✅ Yes (v1.0.0) | ⚠️ Manual | Database location differs, manual copy | +| v1.0.1 | v1.0.0 | ✅ Yes (v1.0.0) | ✅ Yes | Drop-in replacement | +| v1.1.0 | v1.0.0 | ✅ Yes (v1.0.0) | ⚠️ Manual | Database location differs, manual copy | + +### Upgrade Compatibility + +| From Version | To Version | Migration Type | Breaking Changes | Notes | +| ------------ | ---------- | -------------- | ---------------- | ---------------------------------------- | +| v1.0.0 | v1.0.1 | None | No | Drop-in replacement | +| v1.0.1 | v1.1.0 | Automatic | Database path | Auto-migrates Electron → Aquiis folder | +| v0.3.0 | v0.3.1 | Automatic | Database path | Same migration as SimpleStart | +| v1.x.x | v2.0.0 | Automatic | Schema changes | Future: Major version, backup enforced | + +--- + +## Breaking Changes Summary + +| Version | Breaking Changes | Impact | Migration Strategy | +| ------- | -------------------- | -------------------------- | ----------------------- | +| v1.1.0 | Database path change | Transparent to users | Automatic on first run | +| v1.0.1 | None | Backward compatible | Drop-in replacement | +| v1.0.0 | Org structure | Pre-release users only | Manual migration | + +**For detailed migration procedures**, see version-specific Release Notes. + +--- + +## Platform Support + +| Platform | v1.0.0 | v1.0.1 | v1.1.0 | Notes | +| ----------------------- | ------ | ------ | ------ | ----------------------------------- | +| **Linux (AppImage)** | ✅ | ✅ | ✅ | Ubuntu 20.04+, Debian 11+ tested | +| **Windows 10/11 (x64)** | ✅ | ✅ | ✅ | Portable exe, no installer required | +| **macOS** | ❌ | ❌ | ❌ | Planned for v1.2.0+ | + +--- + +## System Requirements + +### Minimum + +- **OS:** Linux (Ubuntu 20.04+) or Windows 10 (64-bit) +- **CPU:** 2-core, 1.5 GHz +- **RAM:** 2 GB +- **Disk:** 500 MB (application + data) + +### Recommended + +- **CPU:** 4-core, 2.5 GHz +- **RAM:** 4 GB +- **Disk:** 1 GB +- **Display:** 1920x1080 + +--- + +## Database Schema Compatibility + +| App Version | Database Schema | Database File | Forward Compatible | Backward Compatible | Notes | +| ----------- | --------------- | -------------------- | ------------------ | ------------------- | ------------------------------ | +| v1.1.0 | v1.0.0 | app_v1.0.0.db | No (path change) | Yes (same schema) | Path: ~/.config/Aquiis/ | +| v1.0.1 | v1.0.0 | app_v1.0.0.db | Yes | Yes | Path: ~/.config/Electron/ | +| v1.0.0 | v1.0.0 | app_v1.0.0.db | Yes | Yes | Path: ~/.config/Electron/ | +| v0.3.0 | v0.0.0 | app_v0.0.0.db | No | No | Pre-release, rapid iteration | + +**Key:** +- **Forward Compatible**: Newer app can open older database +- **Backward Compatible**: Older app can open newer database + +--- + +## Known Limitations + +| Limitation | All Versions | Reason | +| ---------------------- | ---------------------- | ---------------------------- | +| **Maximum Properties** | 9 (SimpleStart) | Simple Start tier constraint | +| **Maximum Users** | 3 (1 system + 3 login) | Simplified access control | +| **Organizations** | 1 | Desktop application scope | +| **File Upload Size** | 10 MB per file | Performance management | +| **SQLite Concurrency** | Single writer | SQLite WAL mode limitation | + +--- + +## Third-Party Licenses + +| Component | License Type | Eligibility Notes | +| ------------ | --------------------- | ------------------------------------------------------------------ | +| **QuestPDF** | Community (Free) | Free for <$1M revenue, individuals, non-profits, FOSS. Honor-based | +| **.NET** | MIT | Open source, commercial use allowed | +| **Bootstrap**| MIT | Open source, commercial use allowed | +| **Electron** | MIT | Open source, commercial use allowed | +| **SendGrid** | Commercial (Optional) | Requires API key and account | +| **Twilio** | Commercial (Optional) | Requires credentials and account | + +**QuestPDF Community License**: SimpleStart (max 9 properties) qualifies as most users will be under $1M annual revenue. Professional edition users must verify eligibility. + +--- + +## Support & Resources + +### Detailed Release Information + +- **v1.1.0:** [Release Notes](v1.1.0/v1.1.0-Release-Notes.md) - What's new, migration procedures, testing +- **v1.0.1:** [Release Notes](v1.0.1/v1.0.1-Release-Notes.md) - Bug fixes and improvements +- **v1.0.0:** [Release Notes](v1.0.0/v1.0.0-Release-Notes.md) - Initial production release + +### Getting Help + +- 📧 **Email:** cisguru@outlook.com +- 🐛 **Bug Reports:** [GitHub Issues](https://github.com/xnodeoncode/Aquiis/issues) +- 💡 **Feature Requests:** [GitHub Discussions](https://github.com/xnodeoncode/Aquiis/discussions) +- 📖 **Documentation:** [/Documentation/](https://github.com/xnodeoncode/Aquiis/tree/main/Documentation) +- 🏛️ **Roadmap:** [/Documentation/Roadmap/](https://github.com/xnodeoncode/Aquiis/tree/main/Documentation/Roadmap) + +--- + +## Change Log + +| Date | Change | Updated By | +| ---------- | ----------------------------------- | ------------ | +| 2026-02-01 | Refocused as Compatibility Matrix | Release Team | +| 2026-02-01 | Added v1.1.0 compatibility info | Release Team | +| 2026-01-29 | Added v1.0.1 entry | Release Team | +| 2026-01-28 | Initial compatibility tracking | Release Team | + +--- + +**Maintained by:** Aquiis Development Team +**Document Version:** 2.0 - Compatibility Matrix From 814b63c568a4528b0f1b2299154c5d9a93d82b5b Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Tue, 3 Feb 2026 02:07:07 -0600 Subject: [PATCH 06/10] database encryption for electron. --- .../Services/DatabaseEncryptionService.cs | 23 +++++- .../Services/LinuxKeychainService.cs | 31 ++++++- .../Extensions/ElectronServiceExtensions.cs | 81 ++++++++++++++++++- .../Extensions/WebServiceExtensions.cs | 2 +- 4-Aquiis.SimpleStart/Program.cs | 13 ++- 5 files changed, 141 insertions(+), 9 deletions(-) diff --git a/1-Aquiis.Infrastructure/Services/DatabaseEncryptionService.cs b/1-Aquiis.Infrastructure/Services/DatabaseEncryptionService.cs index 2b91c69..32da922 100644 --- a/1-Aquiis.Infrastructure/Services/DatabaseEncryptionService.cs +++ b/1-Aquiis.Infrastructure/Services/DatabaseEncryptionService.cs @@ -57,6 +57,10 @@ public DatabaseEncryptionService( SQLitePCL.Batteries_V2.Init(); SQLitePCL.raw.sqlite3_initialize(); + // CRITICAL: Clear connection pools to reset any cached cipher state + SqliteConnection.ClearAllPools(); + _logger.LogInformation("Connection pools cleared before encryption"); + // Attach and copy database using SQLCipher using (var sourceConn = new SqliteConnection($"Data Source={sourcePath}")) { @@ -158,22 +162,34 @@ public DatabaseEncryptionService( if (File.Exists(decryptedPath)) { File.Delete(decryptedPath); + _logger.LogInformation("Deleted existing decrypted database at {Path}", decryptedPath); } // Initialize SQLCipher SQLitePCL.Batteries_V2.Init(); SQLitePCL.raw.sqlite3_initialize(); + // CRITICAL: Clear connection pools to reset any cached cipher state from interceptor + // This is especially important in Electron where the interceptor may have initialized + // SQLCipher with different parameters for the main database + SqliteConnection.ClearAllPools(); + _logger.LogInformation("Connection pools cleared before decryption"); + // Open encrypted database and export to unencrypted + _logger.LogInformation("Creating SqliteConnection for encrypted database: {Path}", encryptedPath); using (var encryptedConn = new SqliteConnection($"Data Source={encryptedPath}")) { + _logger.LogInformation("SqliteConnection object created, calling OpenAsync()..."); + _logger.LogInformation("Password length: {Length} characters", password.Length); await encryptedConn.OpenAsync(); + _logger.LogInformation("✅ Encrypted database opened successfully"); // Set password using PRAGMA using (var cmd = encryptedConn.CreateCommand()) { cmd.CommandText = $"PRAGMA key = '{password}';"; await cmd.ExecuteNonQueryAsync(); + _logger.LogInformation("Encryption key set with PRAGMA"); } // Attach unencrypted database @@ -181,6 +197,7 @@ public DatabaseEncryptionService( { cmd.CommandText = $"ATTACH DATABASE '{decryptedPath}' AS plaintext KEY '';"; await cmd.ExecuteNonQueryAsync(); + _logger.LogInformation("Plaintext database attached"); } // Export schema and data to plaintext database @@ -188,13 +205,16 @@ public DatabaseEncryptionService( { cmd.CommandText = "SELECT sqlcipher_export('plaintext');"; await cmd.ExecuteNonQueryAsync(); + _logger.LogInformation("SQLCipher export to plaintext completed"); } // Detach plaintext database using (var cmd = encryptedConn.CreateCommand()) { cmd.CommandText = "DETACH DATABASE plaintext;"; + await cmd.ExecuteNonQueryAsync(); + _logger.LogInformation("Plaintext database detached"); } } @@ -205,6 +225,7 @@ public DatabaseEncryptionService( if (File.Exists(decryptedPath)) { File.Delete(decryptedPath); + _logger.LogWarning("Deleted existing decrypted database at {Path}", decryptedPath); } return (false, null, "Failed to verify decrypted database"); } @@ -221,7 +242,7 @@ public DatabaseEncryptionService( catch (Exception ex) { _logger.LogError(ex, "Failed to decrypt database"); - return (false, null, $"Decryption failed: {ex.Message}"); + return (false, null, $"Decryption failed: {ex.Message} Inner exception:{ex.InnerException?.Message}"); } } diff --git a/1-Aquiis.Infrastructure/Services/LinuxKeychainService.cs b/1-Aquiis.Infrastructure/Services/LinuxKeychainService.cs index 6491090..5e5a053 100644 --- a/1-Aquiis.Infrastructure/Services/LinuxKeychainService.cs +++ b/1-Aquiis.Infrastructure/Services/LinuxKeychainService.cs @@ -11,7 +11,18 @@ public class LinuxKeychainService { private const string Schema = "org.aquiis.database"; private const string KeyAttribute = "key-type"; - private const string KeyValue = "database-encryption"; + private readonly string _keyValue; + + /// + /// Initialize keychain service with app-specific identifier + /// + /// Application name (e.g., "SimpleStart-Web", "SimpleStart-Electron", "Professional-Web") to prevent keychain conflicts + public LinuxKeychainService(string appName = "Aquiis-Electron") + { + // Make keychain entry unique per application to prevent password conflicts + _keyValue = $"database-encryption-{appName}"; + Console.WriteLine($"[LinuxKeychainService] Initialized with key attribute value: {_keyValue}"); + } /// /// Store encryption key in Linux keychain (libsecret) @@ -33,7 +44,7 @@ public bool StoreKey(string keyHex, string label = "Aquiis Database Encryption K StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "secret-tool", - Arguments = $"store --label=\"{label}\" {KeyAttribute} {KeyValue}", + Arguments = $"store --label=\"{label}\" {KeyAttribute} {_keyValue}", RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, @@ -67,12 +78,13 @@ public bool StoreKey(string keyHex, string label = "Aquiis Database Encryption K try { + Console.WriteLine($"[LinuxKeychainService] Retrieving key with attribute value: {_keyValue}"); var process = new System.Diagnostics.Process { StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "secret-tool", - Arguments = $"lookup {KeyAttribute} {KeyValue}", + Arguments = $"lookup {KeyAttribute} {_keyValue}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, @@ -81,9 +93,20 @@ public bool StoreKey(string keyHex, string label = "Aquiis Database Encryption K }; process.Start(); + + // Read both stdout and stderr to prevent deadlocks var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + process.WaitForExit(5000); + Console.WriteLine($"[LinuxKeychainService] secret-tool exit code: {process.ExitCode}"); + Console.WriteLine($"[LinuxKeychainService] secret-tool output: '{output}'"); + if (!string.IsNullOrWhiteSpace(error)) + { + Console.WriteLine($"[LinuxKeychainService] secret-tool error: {error}"); + } + if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) { return output.Trim(); @@ -114,7 +137,7 @@ public bool RemoveKey() StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "secret-tool", - Arguments = $"clear {KeyAttribute} {KeyValue}", + Arguments = $"clear {KeyAttribute} {_keyValue}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, diff --git a/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs index bcb75be..792b1ee 100644 --- a/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs +++ b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs @@ -12,6 +12,7 @@ using Aquiis.SimpleStart.Entities; using Aquiis.SimpleStart.Services; using Microsoft.Data.Sqlite; +using Aquiis.Infrastructure.Services; namespace Aquiis.SimpleStart.Extensions; @@ -40,8 +41,13 @@ public static IServiceCollection AddElectronServices( var connectionString = GetElectronConnectionString(configuration); // Check if database is encrypted and retrieve password if needed - var encryptionPassword = WebServiceExtensions.GetEncryptionPasswordIfNeeded(connectionString); + var encryptionPassword = GetEncryptionPasswordIfNeeded(connectionString); + if(EnableVerboseLogging) + { + Console.WriteLine("[ElectronServiceExtensions] Connection string obtained. Encryption needed: " + + (!string.IsNullOrEmpty(encryptionPassword)).ToString() + $", Password: {encryptionPassword}"); + } // Register encryption status as singleton for use during startup services.AddSingleton(new EncryptionDetectionResult { @@ -130,6 +136,79 @@ public static IServiceCollection AddElectronServices( return services; } + /// + /// Detects if database is encrypted and retrieves password from keychain if needed + /// + /// Encryption password, or null if database is not encrypted + private static string? GetEncryptionPasswordIfNeeded(string connectionString) + { + try + { + // Extract database path from connection string + var builder = new SqliteConnectionStringBuilder(connectionString); + var dbPath = builder.DataSource; + + if (!File.Exists(dbPath)) + { + // Database doesn't exist yet, not encrypted + return null; + } + + // Try to open as plaintext + try + { + using (var conn = new SqliteConnection(connectionString)) + { + conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master;"; + cmd.ExecuteScalar(); + } + } + // Success - database is not encrypted + return null; + } + catch (SqliteException ex) when (ex.SqliteErrorCode == 26) // "file is not a database" + { + // Database is encrypted - try to get password from keychain + if (EnableVerboseLogging) + Console.WriteLine("Detected encrypted database, retrieving password from keychain..."); + var keychain = new LinuxKeychainService("SimpleStart-Electron"); // Pass app name to prevent keychain conflicts + + Console.WriteLine("Attempting to retrieve encryption password from keychain..."); + var password = keychain.RetrieveKey(); + + if (string.IsNullOrEmpty(password)) + { + throw new InvalidOperationException( + "Database is encrypted but encryption password not found in keychain. " + + "Please restore from an unencrypted backup."); + } + + if (EnableVerboseLogging) + Console.WriteLine($"Encryption password retrieved successfully (length: {password.Length} chars)"); + + // CRITICAL: Clear connection pool to prevent reuse of unencrypted connections + SqliteConnection.ClearAllPools(); + if (EnableVerboseLogging) + Console.WriteLine("Connection pool cleared to force encryption on all new connections"); + + return password; + } + } + catch (InvalidOperationException) + { + throw; // Re-throw our custom messages + } + catch (Exception ex) + { + // Log but don't fail - assume database is not encrypted + Console.WriteLine($"Warning: Could not check database encryption status: {ex.Message}"); + return null; + } + } + /// /// Gets the connection string for Electron mode using the path service synchronously. /// This avoids deadlocks during service registration before Electron is fully initialized. diff --git a/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs index 8b4fc3a..de54899 100644 --- a/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs +++ b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs @@ -162,7 +162,7 @@ public static IServiceCollection AddWebServices( // Database is encrypted - try to get password from keychain if (EnableVerboseLogging) Console.WriteLine("Detected encrypted database, retrieving password from keychain..."); - var keychain = new LinuxKeychainService(); + var keychain = new LinuxKeychainService("SimpleStart-Web"); // Pass app name to prevent keychain conflicts var password = keychain.RetrieveKey(); if (string.IsNullOrEmpty(password)) diff --git a/4-Aquiis.SimpleStart/Program.cs b/4-Aquiis.SimpleStart/Program.cs index 2bba786..b34b3c1 100644 --- a/4-Aquiis.SimpleStart/Program.cs +++ b/4-Aquiis.SimpleStart/Program.cs @@ -180,7 +180,12 @@ // Database encryption services builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(sp => +{ + // Pass app name to prevent keychain conflicts between different apps and modes + var appName = HybridSupport.IsElectronActive ? "SimpleStart-Electron" : "SimpleStart-Web"; + return new LinuxKeychainService(appName); +}); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -229,6 +234,8 @@ { var pathService = scope.ServiceProvider.GetRequiredService(); var dbPath = await pathService.GetDatabasePathAsync(); + + Console.WriteLine($"[Program] Beginning migrations Electron database path: {dbPath}"); // ✅ v1.1.0: Automatic migration from old Electron folder to new Aquiis folder var basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); @@ -284,6 +291,7 @@ if (File.Exists(stagedRestorePath)) { app.Logger.LogInformation("Found staged restore file, applying it now"); + Console.WriteLine($"[Program] Staged restore found, {stagedRestorePath}"); // Backup current database if it exists if (File.Exists(dbPath)) @@ -725,6 +733,7 @@ static void HandlePendingRestore(IConfiguration configuration) { var connectionString = configuration.GetConnectionString("DefaultConnection"); + Console.WriteLine($"[Program.HandlePendingRestore] Checking for staged restore on database connection string: {connectionString}"); if (string.IsNullOrEmpty(connectionString)) { // Can't proceed without connection string @@ -745,7 +754,7 @@ static void HandlePendingRestore(IConfiguration configuration) // Check if there's a staged restore waiting if (File.Exists(stagedRestorePath)) { - Console.WriteLine("Found staged restore file, applying it now..."); + Console.WriteLine($"[Program.HandlePendingRestore] Found staged restore file, applying it now: {stagedRestorePath}"); // Clear SQLite connection pool Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); From 9510711ed2c6d2f9f429468e6cf3f70240a1bdde Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Wed, 11 Feb 2026 20:15:40 -0600 Subject: [PATCH 07/10] security enhancements --- .../Data/ApplicationDbContext.cs | 11 +- ...09120000_FixInvoicePaymentUniqueIndexes.cs | 56 ++ .../Services/DatabaseUnlockState.cs | 16 + 2-Aquiis.Application/DependencyInjection.cs | 2 + .../Models/DTOs/DatabasePreviewDTOs.cs | 76 +++ .../Services/DatabasePreviewService.cs | 263 ++++++++ .../Services/DatabaseUnlockService.cs | 72 +++ .../Services/InvoiceService.cs | 33 +- .../Services/PaymentService.cs | 34 +- .../Services/ScheduledTaskService.cs | 18 + .../Workflows/SampleDataWorkflowService.cs | 568 ++++++++++++++++++ .../Organizations/OrganizationCard.razor | 32 +- .../Organizations/OrganizationDetails.razor | 21 +- .../Aquiis.SimpleStart.csproj | 2 +- .../ElectronHostHook/package-lock.json | 21 + .../Extensions/ElectronServiceExtensions.cs | 64 +- .../Extensions/WebServiceExtensions.cs | 63 +- .../Pages/ViewOrganization.razor | 94 ++- .../Settings/Pages/DatabasePreview.razor | 443 ++++++++++++++ .../Settings/Pages/DatabaseSettings.razor | 14 +- .../Features/DatabaseUnlock/Index.razor | 200 ++++++ 4-Aquiis.SimpleStart/Program.cs | 85 ++- .../Properties/launchSettings.json | 46 +- 4-Aquiis.SimpleStart/Shared/Routes.razor | 32 +- 4-Aquiis.SimpleStart/electron.manifest.json | 4 +- .../wwwroot/js/fileDownload.js | 17 + .../Services/InvoiceServiceTests.cs | 36 +- 27 files changed, 2226 insertions(+), 97 deletions(-) create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260209120000_FixInvoicePaymentUniqueIndexes.cs create mode 100644 1-Aquiis.Infrastructure/Services/DatabaseUnlockState.cs create mode 100644 2-Aquiis.Application/Models/DTOs/DatabasePreviewDTOs.cs create mode 100644 2-Aquiis.Application/Services/DatabasePreviewService.cs create mode 100644 2-Aquiis.Application/Services/DatabaseUnlockService.cs create mode 100644 2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs create mode 100644 4-Aquiis.SimpleStart/ElectronHostHook/package-lock.json create mode 100644 4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabasePreview.razor create mode 100644 4-Aquiis.SimpleStart/Features/DatabaseUnlock/Index.razor diff --git a/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs index ef1dedf..d98e7d6 100644 --- a/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs +++ b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs @@ -176,7 +176,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(i => i.DocumentId) .OnDelete(DeleteBehavior.SetNull); - entity.HasIndex(e => e.InvoiceNumber).IsUnique(); + // Unique constraint on (OrganizationId, InvoiceNumber) for multi-tenant isolation + entity.HasIndex(e => new { e.OrganizationId, e.InvoiceNumber }) + .IsUnique() + .HasDatabaseName("IX_Invoice_OrgId_InvoiceNumber"); + entity.Property(e => e.Amount).HasPrecision(18, 2); entity.Property(e => e.AmountPaid).HasPrecision(18, 2); @@ -200,6 +204,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(p => p.DocumentId) .OnDelete(DeleteBehavior.SetNull); + // Unique constraint on (OrganizationId, PaymentNumber) for multi-tenant isolation + entity.HasIndex(e => new { e.OrganizationId, e.PaymentNumber }) + .IsUnique() + .HasDatabaseName("IX_Payment_OrgId_PaymentNumber"); + entity.Property(e => e.Amount).HasPrecision(18, 2); // Configure relationship with User diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260209120000_FixInvoicePaymentUniqueIndexes.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260209120000_FixInvoicePaymentUniqueIndexes.cs new file mode 100644 index 0000000..a531cc2 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260209120000_FixInvoicePaymentUniqueIndexes.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Infrastructure.Migrations +{ + /// + public partial class FixInvoicePaymentUniqueIndexes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Drop the incorrect single-column unique index on Invoices.InvoiceNumber + // This index was preventing different organizations from using the same invoice numbers + migrationBuilder.DropIndex( + name: "IX_Invoices_InvoiceNumber", + table: "Invoices"); + + // Create correct composite unique index on Invoices(OrganizationId, InvoiceNumber) + // This allows different organizations to have the same invoice number (multi-tenant safe) + migrationBuilder.CreateIndex( + name: "IX_Invoice_OrgId_InvoiceNumber", + table: "Invoices", + columns: new[] { "OrganizationId", "InvoiceNumber" }, + unique: true); + + // Create composite unique index on Payments(OrganizationId, PaymentNumber) + // This ensures payment numbers are unique within each organization + migrationBuilder.CreateIndex( + name: "IX_Payment_OrgId_PaymentNumber", + table: "Payments", + columns: new[] { "OrganizationId", "PaymentNumber" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Drop the composite unique indexes + migrationBuilder.DropIndex( + name: "IX_Invoice_OrgId_InvoiceNumber", + table: "Invoices"); + + migrationBuilder.DropIndex( + name: "IX_Payment_OrgId_PaymentNumber", + table: "Payments"); + + // Restore the original (incorrect) single-column index + migrationBuilder.CreateIndex( + name: "IX_Invoices_InvoiceNumber", + table: "Invoices", + column: "InvoiceNumber", + unique: true); + } + } +} diff --git a/1-Aquiis.Infrastructure/Services/DatabaseUnlockState.cs b/1-Aquiis.Infrastructure/Services/DatabaseUnlockState.cs new file mode 100644 index 0000000..1a2de37 --- /dev/null +++ b/1-Aquiis.Infrastructure/Services/DatabaseUnlockState.cs @@ -0,0 +1,16 @@ +namespace Aquiis.Infrastructure.Services; + +/// +/// Singleton service tracking database encryption unlock state during app lifecycle +/// +public class DatabaseUnlockState +{ + public bool NeedsUnlock { get; set; } + public string? DatabasePath { get; set; } + public string? ConnectionString { get; set; } + + // Event to notify when unlock succeeds + public event Action? OnUnlockSuccess; + + public void NotifyUnlockSuccess() => OnUnlockSuccess?.Invoke(); +} diff --git a/2-Aquiis.Application/DependencyInjection.cs b/2-Aquiis.Application/DependencyInjection.cs index 5bfb2bd..1082f96 100644 --- a/2-Aquiis.Application/DependencyInjection.cs +++ b/2-Aquiis.Application/DependencyInjection.cs @@ -34,6 +34,7 @@ public static IServiceCollection AddApplication( services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -55,6 +56,7 @@ public static IServiceCollection AddApplication( services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/2-Aquiis.Application/Models/DTOs/DatabasePreviewDTOs.cs b/2-Aquiis.Application/Models/DTOs/DatabasePreviewDTOs.cs new file mode 100644 index 0000000..a3eca0c --- /dev/null +++ b/2-Aquiis.Application/Models/DTOs/DatabasePreviewDTOs.cs @@ -0,0 +1,76 @@ +namespace Aquiis.Application.Models.DTOs; + +/// +/// DTO for database preview summary data +/// +public class DatabasePreviewData +{ + public int PropertyCount { get; set; } + public int TenantCount { get; set; } + public int LeaseCount { get; set; } + public int InvoiceCount { get; set; } + public int PaymentCount { get; set; } + + public List Properties { get; set; } = new(); + public List Tenants { get; set; } = new(); + public List Leases { get; set; } = new(); +} + +/// +/// DTO for property preview in read-only database view +/// +public class PropertyPreview +{ + public Guid Id { get; set; } + public string Address { get; set; } = string.Empty; + public string City { get; set; } = string.Empty; + public string State { get; set; } = string.Empty; + public string ZipCode { get; set; } = string.Empty; + public string PropertyType { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public int? Units { get; set; } + public decimal? MonthlyRent { get; set; } +} + +/// +/// DTO for tenant preview in read-only database view +/// +public class TenantPreview +{ + public Guid Id { get; set; } + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string FullName => $"{FirstName} {LastName}"; + public string Email { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public DateTime CreatedOn { get; set; } +} + +/// +/// DTO for lease preview in read-only database view +/// +public class LeasePreview +{ + public Guid Id { get; set; } + public string PropertyAddress { get; set; } = string.Empty; + public string TenantName { get; set; } = string.Empty; + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public decimal MonthlyRent { get; set; } + public string Status { get; set; } = string.Empty; +} + +/// +/// Result object for database operations +/// +public class DatabaseOperationResult +{ + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + + public static DatabaseOperationResult SuccessResult(string message = "Operation successful") + => new() { Success = true, Message = message }; + + public static DatabaseOperationResult FailureResult(string message) + => new() { Success = false, Message = message }; +} diff --git a/2-Aquiis.Application/Services/DatabasePreviewService.cs b/2-Aquiis.Application/Services/DatabasePreviewService.cs new file mode 100644 index 0000000..0f4fc76 --- /dev/null +++ b/2-Aquiis.Application/Services/DatabasePreviewService.cs @@ -0,0 +1,263 @@ +using Aquiis.Application.Models.DTOs; +using Aquiis.Core.Entities; +using Aquiis.Infrastructure.Data; +using Aquiis.Infrastructure.Services; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Aquiis.Application.Services; + +/// +/// Service for previewing backup databases in read-only mode. +/// Allows viewing database contents without overwriting active database. +/// +public class DatabasePreviewService +{ + private readonly LinuxKeychainService _keychain; + private readonly ILogger _logger; + private readonly string _backupDirectory; + + public DatabasePreviewService( + LinuxKeychainService keychain, + ILogger logger) + { + _keychain = keychain; + _logger = logger; + + // Determine backup directory - use standard Data folder + var dataPath = Path.Combine(Directory.GetCurrentDirectory(), "Data"); + _backupDirectory = Path.Combine(dataPath, "Backups"); + } + + /// + /// Check if a backup database file is encrypted + /// + public async Task IsDatabaseEncryptedAsync(string backupFileName) + { + var backupPath = GetBackupFilePath(backupFileName); + + if (!File.Exists(backupPath)) + { + _logger.LogWarning($"Backup file not found: {backupPath}"); + return false; + } + + try + { + // Try to open without password + using var conn = new SqliteConnection($"Data Source={backupPath}"); + await conn.OpenAsync(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT count(*) FROM sqlite_master;"; + await cmd.ExecuteScalarAsync(); + return false; // Opened successfully = not encrypted + } + catch (SqliteException ex) + { + // SQLCipher error codes indicate encryption + if (ex.Message.Contains("file is not a database") || + ex.Message.Contains("file is encrypted") || + ex.SqliteErrorCode == 26) // SQLITE_NOTADB + { + _logger.LogInformation($"Backup database {backupFileName} is encrypted"); + return true; + } + + // Some other error + _logger.LogError(ex, $"Error checking encryption status: {ex.Message}"); + throw; + } + } + + /// + /// Try to get password from keychain (Linux only) + /// + public async Task TryGetKeychainPasswordAsync() + { + if (!OperatingSystem.IsLinux()) + return null; + + await Task.CompletedTask; // Make method async + var key = _keychain.RetrieveKey(); + if (key != null) + { + _logger.LogInformation("Retrieved encryption key from keychain"); + } + return key; + } + + /// + /// Verify that a password can decrypt the backup database + /// + public async Task VerifyPasswordAsync(string backupFileName, string password) + { + var backupPath = GetBackupFilePath(backupFileName); + + try + { + using var conn = new SqliteConnection($"Data Source={backupPath}"); + await conn.OpenAsync(); + + // Apply encryption key + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = $"PRAGMA key = '{password}';"; + await cmd.ExecuteNonQueryAsync(); + } + + // Test if we can read the database + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT count(*) FROM sqlite_master;"; + await cmd.ExecuteScalarAsync(); + } + + return DatabaseOperationResult.SuccessResult("Password verified successfully"); + } + catch (SqliteException ex) + { + _logger.LogWarning($"Password verification failed: {ex.Message}"); + return DatabaseOperationResult.FailureResult("Incorrect password"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error verifying password: {ex.Message}"); + return DatabaseOperationResult.FailureResult($"Error: {ex.Message}"); + } + } + + /// + /// Save password to keychain (overwrites existing) + /// + public async Task SavePasswordToKeychainAsync(string password) + { + if (!OperatingSystem.IsLinux()) + { + _logger.LogWarning("Keychain storage only supported on Linux"); + return; + } + + await Task.CompletedTask; // Make method async + _keychain.StoreKey(password, "Aquiis Database Password"); + _logger.LogInformation("Password saved to keychain"); + } + + /// + /// Get preview data from backup database + /// + public async Task GetPreviewDataAsync(string backupFileName, string? password) + { + var backupPath = GetBackupFilePath(backupFileName); + + if (!File.Exists(backupPath)) + { + throw new FileNotFoundException($"Backup file not found: {backupFileName}"); + } + + // Build connection string + var connectionString = string.IsNullOrEmpty(password) + ? $"Data Source={backupPath}" + : $"Data Source={backupPath}"; + + var options = new DbContextOptionsBuilder() + .UseSqlite(connectionString, sqliteOptions => + { + // Read-only mode + sqliteOptions.CommandTimeout(30); + }) + .Options; + + using var previewContext = new ApplicationDbContext(options); + + // Apply encryption key if password provided + if (!string.IsNullOrEmpty(password)) + { + using var conn = previewContext.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + await conn.OpenAsync(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"PRAGMA key = '{password}';"; + await cmd.ExecuteNonQueryAsync(); + } + + // Load preview data + var previewData = new DatabasePreviewData + { + PropertyCount = await previewContext.Properties.CountAsync(p => !p.IsDeleted), + TenantCount = await previewContext.Tenants.CountAsync(t => !t.IsDeleted), + LeaseCount = await previewContext.Leases.CountAsync(l => !l.IsDeleted), + InvoiceCount = await previewContext.Invoices.CountAsync(i => !i.IsDeleted), + PaymentCount = await previewContext.Payments.CountAsync(p => !p.IsDeleted) + }; + + // Load detailed property data + previewData.Properties = await previewContext.Properties + .Where(p => !p.IsDeleted) + .OrderBy(p => p.Address) + .Take(100) // Limit to first 100 for performance + .Select(p => new PropertyPreview + { + Id = p.Id, + Address = p.Address, + City = p.City, + State = p.State, + ZipCode = p.ZipCode, + PropertyType = p.PropertyType, + Status = p.Status, + Units = null, + MonthlyRent = p.MonthlyRent + }) + .ToListAsync(); + + // Load detailed tenant data + previewData.Tenants = await previewContext.Tenants + .Where(t => !t.IsDeleted) + .OrderBy(t => t.LastName) + .Take(100) // Limit to first 100 for performance + .Select(t => new TenantPreview + { + Id = t.Id, + FirstName = t.FirstName, + LastName = t.LastName, + Email = t.Email, + Phone = t.PhoneNumber, + CreatedOn = t.CreatedOn + }) + .ToListAsync(); + + // Load detailed lease data with related entities + previewData.Leases = await previewContext.Leases + .Where(l => !l.IsDeleted) + .Include(l => l.Property) + .Include(l => l.Tenant) + .OrderByDescending(l => l.StartDate) + .Take(100) // Limit to first 100 for performance + .Select(l => new LeasePreview + { + Id = l.Id, + PropertyAddress = l.Property != null ? l.Property.Address : "Unknown", + TenantName = l.Tenant != null ? $"{l.Tenant.FirstName} {l.Tenant.LastName}" : "Unknown", + StartDate = l.StartDate, + EndDate = l.EndDate, + MonthlyRent = l.MonthlyRent, + Status = l.Status + }) + .ToListAsync(); + + _logger.LogInformation($"Loaded preview data: {previewData.PropertyCount} properties, {previewData.TenantCount} tenants, {previewData.LeaseCount} leases"); + + return previewData; + } + + /// + /// Get full path to backup file + /// + private string GetBackupFilePath(string backupFileName) + { + // Security: Prevent path traversal attacks + var safeFileName = Path.GetFileName(backupFileName); + return Path.Combine(_backupDirectory, safeFileName); + } +} diff --git a/2-Aquiis.Application/Services/DatabaseUnlockService.cs b/2-Aquiis.Application/Services/DatabaseUnlockService.cs new file mode 100644 index 0000000..c1a13c6 --- /dev/null +++ b/2-Aquiis.Application/Services/DatabaseUnlockService.cs @@ -0,0 +1,72 @@ +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; +using Aquiis.Infrastructure.Services; + +namespace Aquiis.Application.Services; + +/// +/// Service for unlocking encrypted databases by prompting user for password +/// and verifying it against the database +/// +public class DatabaseUnlockService +{ + private readonly LinuxKeychainService _keychain; + private readonly ILogger _logger; + + public DatabaseUnlockService( + LinuxKeychainService keychain, + ILogger logger) + { + _keychain = keychain; + _logger = logger; + } + + /// + /// Verify password can decrypt the database and store in keychain + /// + /// Database connection string + /// User-provided password to verify + /// (Success, ErrorMessage) + public async Task<(bool Success, string? ErrorMessage)> UnlockDatabaseAsync( + string connectionString, + string password) + { + try + { + _logger.LogInformation("Attempting database unlock verification"); + + // Try to open database with password + using var conn = new SqliteConnection($"{connectionString};Password={password}"); + await conn.OpenAsync(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master;"; + await cmd.ExecuteScalarAsync(); + + _logger.LogInformation("Password verification successful"); + + // Store password in keychain for future use + var stored = _keychain.StoreKey(password, "Aquiis Database Encryption Password"); + if (stored) + { + _logger.LogInformation("Password stored in keychain successfully"); + } + else + { + _logger.LogWarning("Failed to store password in keychain"); + } + + return (true, null); + } + catch (SqliteException ex) when (ex.SqliteErrorCode == 26) + { + _logger.LogWarning("Incorrect password provided"); + return (false, "Incorrect password. Please try again."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error unlocking database"); + return (false, $"Error unlocking database: {ex.Message}"); + } + } +} diff --git a/2-Aquiis.Application/Services/InvoiceService.cs b/2-Aquiis.Application/Services/InvoiceService.cs index 12dcaf5..76fc42f 100644 --- a/2-Aquiis.Application/Services/InvoiceService.cs +++ b/2-Aquiis.Application/Services/InvoiceService.cs @@ -260,17 +260,38 @@ public async Task> GetInvoicesDueSoonAsync(int daysThreshold = 7) /// Generates a unique invoice number for the organization. /// Format: INV-YYYYMM-00001 /// + /// + /// Generates invoice number in format: INV-{YYYYMM}-000n + /// Numbers reset monthly and are scoped to organization. + /// Uses MAX query to get last invoice number for current month. + /// public async Task GenerateInvoiceNumberAsync() { try { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - var invoiceCount = await _context.Invoices - .Where(i => i.OrganizationId == organizationId) - .CountAsync(); - - var nextNumber = invoiceCount + 1; - return $"INV-{DateTime.Now:yyyyMM}-{nextNumber:D5}"; + var yearMonth = DateTime.UtcNow.ToString("yyyyMM"); + + // Get the highest invoice number for this month + var lastInvoice = await _context.Invoices + .Where(i => i.OrganizationId == organizationId + && i.InvoiceNumber.StartsWith($"INV-{yearMonth}")) + .OrderByDescending(i => i.InvoiceNumber) + .Select(i => i.InvoiceNumber) + .FirstOrDefaultAsync(); + + int nextNum = 1; + if (lastInvoice != null) + { + // Extract sequence number from: INV-202602-0001 + var parts = lastInvoice.Split('-'); + if (parts.Length == 3) + { + nextNum = int.Parse(parts[2]) + 1; + } + } + + return $"INV-{yearMonth}-{nextNum:D4}"; } catch (Exception ex) { diff --git a/2-Aquiis.Application/Services/PaymentService.cs b/2-Aquiis.Application/Services/PaymentService.cs index 2727def..eebf32a 100644 --- a/2-Aquiis.Application/Services/PaymentService.cs +++ b/2-Aquiis.Application/Services/PaymentService.cs @@ -377,21 +377,39 @@ public async Task GetTotalPaidForInvoiceAsync(Guid invoiceId) /// /// Generates a unique payment number in the format PYMT-YYYYMMDD-####. /// + /// + /// Generates payment number in format: PYMT-{YYYYMM}-000n + /// Numbers reset monthly and are scoped to organization. + /// Uses MAX query to get last payment number for current month. + /// Format matches invoice numbering for consistency. + /// public async Task GeneratePaymentNumberAsync() { try { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - var today = DateTime.Now.Date; + var yearMonth = DateTime.UtcNow.ToString("yyyyMM"); - // Get count of payments for today to generate sequential number - var todayPaymentCount = await _context.Payments + // Get the highest payment number for this month + var lastPayment = await _context.Payments .Where(p => p.OrganizationId == organizationId - && p.PaidOn.Date == today) - .CountAsync(); - - var nextNumber = todayPaymentCount + 1; - return $"PYMT-{DateTime.Now:yyyyMMdd}-{nextNumber:D4}"; + && p.PaymentNumber.StartsWith($"PYMT-{yearMonth}")) + .OrderByDescending(p => p.PaymentNumber) + .Select(p => p.PaymentNumber) + .FirstOrDefaultAsync(); + + int nextNum = 1; + if (lastPayment != null) + { + // Extract sequence number from: PYMT-202602-0001 + var parts = lastPayment.Split('-'); + if (parts.Length == 3) + { + nextNum = int.Parse(parts[2]) + 1; + } + } + + return $"PYMT-{yearMonth}-{nextNum:D4}"; } catch (Exception ex) { diff --git a/2-Aquiis.Application/Services/ScheduledTaskService.cs b/2-Aquiis.Application/Services/ScheduledTaskService.cs index c221642..5564e43 100644 --- a/2-Aquiis.Application/Services/ScheduledTaskService.cs +++ b/2-Aquiis.Application/Services/ScheduledTaskService.cs @@ -1,5 +1,6 @@ using Aquiis.Core.Constants; using Aquiis.Core.Entities; +using Aquiis.Infrastructure.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; @@ -95,6 +96,14 @@ private async Task DoWork(CancellationToken stoppingToken) using (var scope = _serviceProvider.CreateScope()) { + // Check if database is locked - skip all tasks if so + var unlockState = scope.ServiceProvider.GetService(); + if (unlockState?.NeedsUnlock == true) + { + _logger.LogWarning("Database locked - skipping scheduled tasks. Tasks will resume after unlock."); + return; + } + var dbContext = scope.ServiceProvider.GetRequiredService(); var organizationService = scope.ServiceProvider.GetRequiredService(); var leaseNotificationService = scope.ServiceProvider.GetRequiredService(); @@ -394,6 +403,15 @@ private async Task ExecuteHourlyTasks() try { using var scope = _serviceProvider.CreateScope(); + + // Check if database is locked - skip all tasks if so + var unlockState = scope.ServiceProvider.GetService(); + if (unlockState?.NeedsUnlock == true) + { + _logger.LogWarning("Database locked - skipping hourly tasks. Tasks will resume after unlock."); + return; + } + var tourService = scope.ServiceProvider.GetRequiredService(); var leaseService = scope.ServiceProvider.GetRequiredService(); var dbContext = scope.ServiceProvider.GetRequiredService(); diff --git a/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs b/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs new file mode 100644 index 0000000..9fbba45 --- /dev/null +++ b/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs @@ -0,0 +1,568 @@ +using Aquiis.Application.Services.Workflows; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Aquiis.Application.Services.Workflows +{ + /// + /// Workflow service for generating sample test data. + /// Creates properties, tenants, leases, invoices, and payments for testing and demos. + /// + public class SampleDataWorkflowService : BaseWorkflowService + { + private readonly ILogger _logger; + private readonly InvoiceService _invoiceService; + private readonly PaymentService _paymentService; + private readonly Random _random; + + public SampleDataWorkflowService( + ApplicationDbContext context, + IUserContextService userContext, + InvoiceService invoiceService, + PaymentService paymentService, + ILogger logger) : base(context, userContext) + { + _logger = logger; + _invoiceService = invoiceService; + _paymentService = paymentService; + _random = new Random(DateTime.Now.Millisecond); // Seed for varied data + } + + /// + /// Main orchestration method - generates complete sample dataset. + /// + public async Task GenerateSampleDataAsync() + { + return await ExecuteWorkflowAsync(async () => + { + _logger.LogInformation("Starting sample data generation..."); + + try + { + // Get context + var orgId = await GetActiveOrganizationIdAsync(); + + if (orgId == Guid.Empty) + { + return WorkflowResult.Fail("Organization context not available. Please ensure you are logged in."); + } + + // Use SystemUser.Id for test data identification + var systemUserId = ApplicationConstants.SystemUser.Id; + _logger.LogInformation($"Generating sample data for Organization: {orgId}, CreatedBy: {systemUserId}"); + + // Generate entities in proper order (respecting dependencies) + var properties = await GeneratePropertiesAsync(orgId, systemUserId); + _logger.LogInformation($"Created {properties.Count} properties"); + + var tenants = await GenerateTenantsAsync(orgId, systemUserId); + _logger.LogInformation($"Created {tenants.Count} tenants"); + + var leases = await GenerateLeasesAsync(properties, tenants, orgId, systemUserId); + _logger.LogInformation($"Created {leases.Count} leases"); + + var invoices = await GenerateInvoicesAsync(leases, orgId, systemUserId); + _logger.LogInformation($"Created {invoices.Count} invoices"); + + var payments = await GeneratePaymentsAsync(invoices, orgId, systemUserId); + _logger.LogInformation($"Created {payments.Count} payments"); + + // Log workflow completion + await LogTransitionAsync( + entityType: "SampleData", + entityId: orgId, + fromStatus: null, + toStatus: "Generated", + action: "GenerateSampleData", + reason: $"Created {properties.Count} properties, {tenants.Count} tenants, {leases.Count} leases, {invoices.Count} invoices, {payments.Count} payments" + ); + + return WorkflowResult.Ok( + $"Successfully generated sample data: {properties.Count} properties, {tenants.Count} tenants, " + + $"{leases.Count} leases, {invoices.Count} invoices, {payments.Count} payments"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating sample data"); + return WorkflowResult.Fail($"Error generating sample data: {ex.Message}"); + } + }); + } + + /// + /// Removes all sample data created with SystemUser.Id. + /// Deletes properties, tenants, leases, invoices, and payments in proper order. + /// + public async Task RemoveSampleDataAsync() + { + return await ExecuteWorkflowAsync(async () => + { + _logger.LogInformation("Starting sample data removal..."); + + try + { + var orgId = await GetActiveOrganizationIdAsync(); + + if (orgId == Guid.Empty) + { + return WorkflowResult.Fail("Organization context not available."); + } + + var systemUserId = ApplicationConstants.SystemUser.Id; + _logger.LogInformation($"Removing sample data for Organization: {orgId}, CreatedBy: {systemUserId}"); + + // Delete in reverse order of dependencies + var paymentsDeleted = await RemovePaymentsAsync(orgId, systemUserId); + _logger.LogInformation($"Deleted {paymentsDeleted} payments"); + + var invoicesDeleted = await RemoveInvoicesAsync(orgId, systemUserId); + _logger.LogInformation($"Deleted {invoicesDeleted} invoices"); + + var leasesDeleted = await RemoveLeasesAsync(orgId, systemUserId); + _logger.LogInformation($"Deleted {leasesDeleted} leases"); + + var tenantsDeleted = await RemoveTenantsAsync(orgId, systemUserId); + _logger.LogInformation($"Deleted {tenantsDeleted} tenants"); + + var propertiesDeleted = await RemovePropertiesAsync(orgId, systemUserId); + _logger.LogInformation($"Deleted {propertiesDeleted} properties"); + + // Log workflow completion + await LogTransitionAsync( + entityType: "SampleData", + entityId: orgId, + fromStatus: "Generated", + toStatus: "Removed", + action: "RemoveSampleData", + reason: $"Deleted {propertiesDeleted} properties, {tenantsDeleted} tenants, {leasesDeleted} leases, {invoicesDeleted} invoices, {paymentsDeleted} payments" + ); + + return WorkflowResult.Ok( + $"Successfully removed sample data: {propertiesDeleted} properties, {tenantsDeleted} tenants, " + + $"{leasesDeleted} leases, {invoicesDeleted} invoices, {paymentsDeleted} payments"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error removing sample data"); + return WorkflowResult.Fail($"Error removing sample data: {ex.Message}"); + } + }); + } + + #region Property Generation + + private async Task> GeneratePropertiesAsync(Guid organizationId, string userId) + { + var properties = new List(); + var now = DateTime.UtcNow; + + // Define 6 properties in Texas with varied characteristics + var propertyData = new[] + { + new { Address = "1234 Riverside Dr", City = "Austin", State = "TX", Zip = "78701", Type = ApplicationConstants.PropertyTypes.House, Beds = 3, Baths = 2.0m, SqFt = 1850, Rent = 1850m, Status = ApplicationConstants.PropertyStatuses.Occupied }, + new { Address = "5678 Oak Street", City = "Houston", State = "TX", Zip = "77002", Type = ApplicationConstants.PropertyTypes.Apartment, Beds = 2, Baths = 2.0m, SqFt = 1200, Rent = 1450m, Status = ApplicationConstants.PropertyStatuses.Occupied }, + new { Address = "910 Maple Ave", City = "Dallas", State = "TX", Zip = "75201", Type = ApplicationConstants.PropertyTypes.House, Beds = 4, Baths = 3.0m, SqFt = 2500, Rent = 2200m, Status = ApplicationConstants.PropertyStatuses.Occupied }, + new { Address = "1122 Pine Ln", City = "San Antonio", State = "TX", Zip = "78205", Type = ApplicationConstants.PropertyTypes.Condo, Beds = 2, Baths = 1.0m, SqFt = 1100, Rent = 1200m, Status = ApplicationConstants.PropertyStatuses.Available }, + new { Address = "3344 Elm Ct", City = "Fort Worth", State = "TX", Zip = "76102", Type = ApplicationConstants.PropertyTypes.House, Beds = 3, Baths = 2.0m, SqFt = 1750, Rent = 1750m, Status = ApplicationConstants.PropertyStatuses.Available }, + new { Address = "5566 Cedar Rd", City = "El Paso", State = "TX", Zip = "79901", Type = ApplicationConstants.PropertyTypes.Apartment, Beds = 1, Baths = 1.0m, SqFt = 850, Rent = 1100m, Status = ApplicationConstants.PropertyStatuses.Available } + }; + + for (int i = 0; i < propertyData.Length; i++) + { + var data = propertyData[i]; + var createdDate = GetRandomDate(new DateTime(2025, 4, 1), new DateTime(2025, 6, 30)); + + var property = new Property + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Address = data.Address, + City = data.City, + State = data.State, + ZipCode = data.Zip, + PropertyType = data.Type, + Bedrooms = data.Beds, + Bathrooms = data.Baths, + SquareFeet = data.SqFt, + MonthlyRent = data.Rent, + Status = data.Status, + IsAvailable = data.Status == ApplicationConstants.PropertyStatuses.Available, + Description = $"Beautiful {data.Beds} bedroom, {data.Baths} bath {data.Type.ToLower()} in {data.City}. " + + $"{data.SqFt} square feet with modern amenities and convenient location.", + RoutineInspectionIntervalMonths = 12, + NextRoutineInspectionDueDate = DateTime.Today.AddMonths(6), + CreatedBy = userId, + CreatedOn = createdDate, + IsDeleted = false + }; + + _context.Properties.Add(property); + properties.Add(property); + } + + await _context.SaveChangesAsync(); + _logger.LogInformation($"Generated {properties.Count} properties"); + + return properties; + } + + #endregion + + #region Tenant Generation + + private async Task> GenerateTenantsAsync(Guid organizationId, string userId) + { + var tenants = new List(); + + // Define 3 tenants with realistic data + var tenantData = new[] + { + new { FirstName = "Sarah", LastName = "Johnson", DOB = new DateTime(1988, 5, 15), EmergencyName = "John Johnson", EmergencyPhone = "555-987-6543", Relationship = "Spouse" }, + new { FirstName = "Michael", LastName = "Chen", DOB = new DateTime(1992, 8, 22), EmergencyName = "Lisa Chen", EmergencyPhone = "555-876-5432", Relationship = "Sister" }, + new { FirstName = "Emily", LastName = "Rodriguez", DOB = new DateTime(1990, 3, 10), EmergencyName = "Carlos Rodriguez", EmergencyPhone = "555-765-4321", Relationship = "Father" } + }; + + for (int i = 0; i < tenantData.Length; i++) + { + var data = tenantData[i]; + var createdDate = GetRandomDate(new DateTime(2025, 5, 1), new DateTime(2025, 7, 31)); + + var tenant = new Tenant + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + FirstName = data.FirstName, + LastName = data.LastName, + Email = $"{data.FirstName.ToLower()}.{data.LastName.ToLower()}@example.com", + PhoneNumber = $"555-{_random.Next(100, 999)}-{_random.Next(1000, 9999)}", + DateOfBirth = data.DOB, + IdentificationNumber = $"DL-{_random.Next(10000000, 99999999)}", + IsActive = true, + EmergencyContactName = data.EmergencyName, + EmergencyContactPhone = data.EmergencyPhone, + Notes = $"Emergency contact relationship: {data.Relationship}", + CreatedBy = userId, + CreatedOn = createdDate, + IsDeleted = false + }; + + _context.Tenants.Add(tenant); + tenants.Add(tenant); + } + + await _context.SaveChangesAsync(); + _logger.LogInformation($"Generated {tenants.Count} tenants"); + + return tenants; + } + + #endregion + + #region Lease Generation + + private async Task> GenerateLeasesAsync( + List properties, + List tenants, + Guid organizationId, + string userId) + { + var leases = new List(); + + // Create 3 leases for first 3 properties + var leaseStartMonths = new[] { 5, 6, 7 }; // May, June, July 2025 + + for (int i = 0; i < 3; i++) + { + var property = properties[i]; + var tenant = tenants[i]; + var startMonth = leaseStartMonths[i]; + + // Random start day (1-5 of month) + var startDay = _random.Next(1, 6); + var startDate = new DateTime(2025, startMonth, startDay); + var endDate = startDate.AddYears(1); // 12-month lease + + var lease = new Lease + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + PropertyId = property.Id, + TenantId = tenant.Id, + StartDate = startDate, + EndDate = endDate, + MonthlyRent = property.MonthlyRent, + SecurityDeposit = property.MonthlyRent, // 1x rent + Status = ApplicationConstants.LeaseStatuses.Active, + Terms = $"12-month {ApplicationConstants.LeaseTypes.FixedTerm} lease. Rent: ${property.MonthlyRent}/month. " + + $"Security Deposit: ${property.MonthlyRent}. Payment due on the 5th of each month.", + SignedOn = startDate.AddDays(-10), // Signed 10 days before start + OfferedOn = startDate.AddDays(-20), // Offered 20 days before start + CreatedBy = userId, + CreatedOn = startDate.AddDays(-25), + IsDeleted = false + }; + + _context.Leases.Add(lease); + leases.Add(lease); + + // Update property status to Occupied + property.Status = ApplicationConstants.PropertyStatuses.Occupied; + property.IsAvailable = false; + } + + await _context.SaveChangesAsync(); + _logger.LogInformation($"Generated {leases.Count} leases"); + + return leases; + } + + #endregion + + #region Invoice Generation + + private async Task> GenerateInvoicesAsync( + List leases, + Guid organizationId, + string userId) + { + var invoices = new List(); + var currentDate = DateTime.UtcNow.Date; + + foreach (var lease in leases) + { + var invoiceDate = new DateTime(lease.StartDate.Year, lease.StartDate.Month, 20); // 20th of start month + var currentMonth = new DateTime(currentDate.Year, currentDate.Month, 1); + + // Generate invoices from lease start to current month + while (invoiceDate.Month <= currentDate.Month || invoiceDate.Year < currentDate.Year) + { + // Skip if invoice date is in the future + if (invoiceDate > currentDate) + break; + + var dueDate = invoiceDate.AddMonths(1); + dueDate = new DateTime(dueDate.Year, dueDate.Month, 5); // Due on 5th of following month + + // Generate proper invoice number using service + var invoiceNumber = await _invoiceService.GenerateInvoiceNumberAsync(); + + var invoice = new Invoice + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + LeaseId = lease.Id, + InvoiceNumber = invoiceNumber, + InvoicedOn = invoiceDate, + DueOn = dueDate, + Amount = lease.MonthlyRent, + Status = ApplicationConstants.InvoiceStatuses.Pending, + Description = $"Monthly Rent - {invoiceDate:MMMM yyyy}", + CreatedBy = userId, + CreatedOn = invoiceDate, + IsDeleted = false + }; + + _context.Invoices.Add(invoice); + + // CRITICAL FIX: Save immediately after generating number to prevent collisions + // The MAX query in GenerateInvoiceNumberAsync needs to see this invoice + // before generating the next number + await _context.SaveChangesAsync(); + + invoices.Add(invoice); + + // Move to next month + invoiceDate = invoiceDate.AddMonths(1); + } + } + + _logger.LogInformation($"Generated {invoices.Count} invoices"); + + return invoices; + } + + #endregion + + #region Payment Generation + + private async Task> GeneratePaymentsAsync( + List invoices, + Guid organizationId, + string userId) + { + var payments = new List(); + var currentDate = DateTime.UtcNow.Date; + + // Group invoices by lease to check remaining months + var invoicesByLease = invoices.GroupBy(i => i.LeaseId).ToList(); + + foreach (var leaseGroup in invoicesByLease) + { + var leaseInvoices = leaseGroup.OrderBy(i => i.InvoicedOn).ToList(); + + // Calculate months remaining (leases end in May/June/July 2026) + var lastInvoice = leaseInvoices.Last(); + var lease = await _context.Leases.FindAsync(leaseGroup.Key); + + if (lease == null) continue; + + var monthsRemaining = ((lease.EndDate.Year - currentDate.Year) * 12) + + lease.EndDate.Month - currentDate.Month; + + // If >3 months remaining, create payments for last 3 months only + if (monthsRemaining > 3) + { + // Get last 3 invoices that have passed their due date + var recentInvoices = leaseInvoices + .Where(i => i.DueOn < currentDate) + .OrderByDescending(i => i.InvoicedOn) + .Take(3) + .ToList(); + + foreach (var invoice in recentInvoices) + { + // Payment made 1-4 days before due date + var paymentDate = invoice.DueOn.AddDays(-_random.Next(1, 5)); + + // Generate proper payment number using service + var paymentNumber = await _paymentService.GeneratePaymentNumberAsync(); + + var payment = new Payment + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + InvoiceId = invoice.Id, + Amount = invoice.Amount, + PaymentNumber = paymentNumber, + PaidOn = paymentDate, + PaymentMethod = GetRandomPaymentMethod(), + Notes = $"Payment for {invoice.Description}", + CreatedBy = userId, + CreatedOn = paymentDate, + IsDeleted = false + }; + + _context.Payments.Add(payment); + + // CRITICAL FIX: Save immediately after generating number to prevent collisions + await _context.SaveChangesAsync(); + + payments.Add(payment); + + // Update invoice status to Paid + invoice.Status = ApplicationConstants.InvoiceStatuses.Paid; + invoice.AmountPaid = invoice.Amount; + invoice.PaidOn = paymentDate; + invoice.LastModifiedBy = userId; + invoice.LastModifiedOn = paymentDate; + + // Save invoice status update immediately + await _context.SaveChangesAsync(); + } + } + } + + _logger.LogInformation($"Generated {payments.Count} payments"); + + return payments; + } + + #endregion + + #region Helper Methods + + /// + /// Gets a random date within the specified range. + /// + private DateTime GetRandomDate(DateTime start, DateTime end) + { + var range = (end - start).Days; + return start.AddDays(_random.Next(range + 1)); + } + + /// + /// Gets a random payment method from available options. + /// + private string GetRandomPaymentMethod() + { + var methods = new[] + { + ApplicationConstants.PaymentMethods.CreditCard, + ApplicationConstants.PaymentMethods.Check, + ApplicationConstants.PaymentMethods.BankTransfer, + ApplicationConstants.PaymentMethods.Cash + }; + + return methods[_random.Next(methods.Length)]; + } + + #endregion + + #region Sample Data Removal + + private async Task RemovePropertiesAsync(Guid organizationId, string systemUserId) + { + var properties = await _context.Properties + .Where(p => p.OrganizationId == organizationId && p.CreatedBy == systemUserId) + .ToListAsync(); + + _context.Properties.RemoveRange(properties); + await _context.SaveChangesAsync(); + + return properties.Count; + } + + private async Task RemoveTenantsAsync(Guid organizationId, string systemUserId) + { + var tenants = await _context.Tenants + .Where(t => t.OrganizationId == organizationId && t.CreatedBy == systemUserId) + .ToListAsync(); + + _context.Tenants.RemoveRange(tenants); + await _context.SaveChangesAsync(); + + return tenants.Count; + } + + private async Task RemoveLeasesAsync(Guid organizationId, string systemUserId) + { + var leases = await _context.Leases + .Where(l => l.OrganizationId == organizationId && l.CreatedBy == systemUserId) + .ToListAsync(); + + _context.Leases.RemoveRange(leases); + await _context.SaveChangesAsync(); + + return leases.Count; + } + + private async Task RemoveInvoicesAsync(Guid organizationId, string systemUserId) + { + var invoices = await _context.Invoices + .Where(i => i.OrganizationId == organizationId && i.CreatedBy == systemUserId) + .ToListAsync(); + + _context.Invoices.RemoveRange(invoices); + await _context.SaveChangesAsync(); + + return invoices.Count; + } + + private async Task RemovePaymentsAsync(Guid organizationId, string systemUserId) + { + var payments = await _context.Payments + .Where(p => p.OrganizationId == organizationId && p.CreatedBy == systemUserId) + .ToListAsync(); + + _context.Payments.RemoveRange(payments); + await _context.SaveChangesAsync(); + + return payments.Count; + } + + #endregion + } +} diff --git a/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor index e5f767e..66a6835 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor @@ -8,9 +8,20 @@
Organization Information
- +
+ + @if (IsOwner) + { + + + } +
@if(OrganizationViewModel?.Organization != null) { @@ -69,4 +80,19 @@ [Parameter] public EventCallback OnEdit { get; set; } + [Parameter] + public EventCallback OnGenerateSampleData { get; set; } + + [Parameter] + public EventCallback OnRemoveSampleData { get; set; } + + [Parameter] + public bool IsOwner { get; set; } + + [Parameter] + public bool IsGeneratingSample { get; set; } + + [Parameter] + public bool IsRemovingSample { get; set; } + } \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationDetails.razor b/3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationDetails.razor index 929f411..b548e93 100644 --- a/3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationDetails.razor +++ b/3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationDetails.razor @@ -10,7 +10,14 @@
- + OrganizationViewModel?.Organization; public List OrganizationUsers => OrganizationViewModel?.OrganizationUsers ?? new List(); diff --git a/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj b/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj index eb73404..fa508a7 100644 --- a/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj +++ b/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj @@ -15,7 +15,7 @@ 1.1.0 - + PreserveNewest diff --git a/4-Aquiis.SimpleStart/ElectronHostHook/package-lock.json b/4-Aquiis.SimpleStart/ElectronHostHook/package-lock.json new file mode 100644 index 0000000..b4925d3 --- /dev/null +++ b/4-Aquiis.SimpleStart/ElectronHostHook/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "aquiis-simplestart-electron", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aquiis-simplestart-electron", + "version": "1.0.0", + "dependencies": { + "dasherize": "^2.0.0" + } + }, + "node_modules/dasherize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", + "integrity": "sha512-APql/TZ6FdLEpf2z7/X2a2zyqK8juYtqaSVqxw9mYoQ64CXkfU15AeLh8pUszT8+fnYjgm6t0aIYpWKJbnLkuA==", + "license": "MIT" + } + } +} diff --git a/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs index 792b1ee..ef43cd4 100644 --- a/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs +++ b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs @@ -10,9 +10,9 @@ using Aquiis.Infrastructure.Data; // For SqlCipherConnectionInterceptor using Aquiis.SimpleStart.Data; using Aquiis.SimpleStart.Entities; -using Aquiis.SimpleStart.Services; +using Aquiis.SimpleStart.Services; // For ElectronPathService, WebPathService +using Aquiis.Infrastructure.Services; // For DatabaseUnlockState using Microsoft.Data.Sqlite; -using Aquiis.Infrastructure.Services; namespace Aquiis.SimpleStart.Extensions; @@ -48,12 +48,29 @@ public static IServiceCollection AddElectronServices( Console.WriteLine("[ElectronServiceExtensions] Connection string obtained. Encryption needed: " + (!string.IsNullOrEmpty(encryptionPassword)).ToString() + $", Password: {encryptionPassword}"); } + + // Register unlock state before any DbContext registration + var unlockState = new DatabaseUnlockState + { + NeedsUnlock = encryptionPassword == null && IsDatabaseEncrypted(connectionString), + DatabasePath = ExtractDatabasePath(connectionString), + ConnectionString = connectionString + }; + services.AddSingleton(unlockState); + // Register encryption status as singleton for use during startup services.AddSingleton(new EncryptionDetectionResult { IsEncrypted = !string.IsNullOrEmpty(encryptionPassword) }); + // If unlock needed, we still register services (so DI doesn't fail) + // but they won't be able to access database until password is provided + if (unlockState.NeedsUnlock) + { + Console.WriteLine("[ElectronServiceExtensions] Database unlock required - services will be registered but database inaccessible until unlock"); + } + // CRITICAL: Create interceptor instance BEFORE any DbContext registration // This single instance will be used by all DbContexts SqlCipherConnectionInterceptor? interceptor = null; @@ -181,9 +198,8 @@ public static IServiceCollection AddElectronServices( if (string.IsNullOrEmpty(password)) { - throw new InvalidOperationException( - "Database is encrypted but encryption password not found in keychain. " + - "Please restore from an unencrypted backup."); + Console.WriteLine("Database is encrypted but password not in keychain - will prompt user"); + return null; // Signal that unlock is needed } if (EnableVerboseLogging) @@ -219,4 +235,42 @@ private static string GetElectronConnectionString(IConfiguration configuration) var dbPath = pathService.GetDatabasePathSync(); return $"DataSource={dbPath};Cache=Shared"; } + + /// + /// Helper method to check if database is encrypted + /// + private static bool IsDatabaseEncrypted(string connectionString) + { + var builder = new SqliteConnectionStringBuilder(connectionString); + var dbPath = builder.DataSource; + + if (!File.Exists(dbPath)) return false; + + try + { + using var conn = new SqliteConnection(connectionString); + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master;"; + cmd.ExecuteScalar(); + return false; // Opened successfully = not encrypted + } + catch (SqliteException ex) when (ex.SqliteErrorCode == 26) + { + return true; // Error 26 = encrypted + } + catch + { + return false; // Other errors = assume not encrypted + } + } + + /// + /// Helper method to extract database path from connection string + /// + private static string ExtractDatabasePath(string connectionString) + { + var builder = new SqliteConnectionStringBuilder(connectionString); + return builder.DataSource; + } } diff --git a/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs index de54899..f231ca7 100644 --- a/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs +++ b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs @@ -10,8 +10,8 @@ using Aquiis.Infrastructure.Data; // For SqlCipherConnectionInterceptor using Aquiis.SimpleStart.Data; using Aquiis.SimpleStart.Entities; -using Aquiis.SimpleStart.Services; -using Aquiis.Infrastructure.Services; +using Aquiis.SimpleStart.Services; // For ElectronPathService, WebPathService +using Aquiis.Infrastructure.Services; // For DatabaseUnlockState using Microsoft.Data.Sqlite; namespace Aquiis.SimpleStart.Extensions; @@ -48,12 +48,28 @@ public static IServiceCollection AddWebServices( // Check if database is encrypted and retrieve password if needed var encryptionPassword = GetEncryptionPasswordIfNeeded(connectionString); + // Register unlock state before any DbContext registration + var unlockState = new DatabaseUnlockState + { + NeedsUnlock = encryptionPassword == null && IsDatabaseEncrypted(connectionString), + DatabasePath = ExtractDatabasePath(connectionString), + ConnectionString = connectionString + }; + services.AddSingleton(unlockState); + // Register encryption status as singleton for use during startup services.AddSingleton(new EncryptionDetectionResult { IsEncrypted = !string.IsNullOrEmpty(encryptionPassword) }); + // If unlock needed, we still register services (so DI doesn't fail) + // but they won't be able to access database until password is provided + if (unlockState.NeedsUnlock) + { + Console.WriteLine("Database unlock required - services will be registered but database inaccessible until unlock"); + } + // CRITICAL: Create interceptor instance BEFORE any DbContext registration // This single instance will be used by all DbContexts SqlCipherConnectionInterceptor? interceptor = null; @@ -167,9 +183,8 @@ public static IServiceCollection AddWebServices( if (string.IsNullOrEmpty(password)) { - throw new InvalidOperationException( - "Database is encrypted but encryption password not found in keychain. " + - "Please restore from an unencrypted backup."); + Console.WriteLine("Database is encrypted but password not in keychain - will prompt user"); + return null; // Signal that unlock is needed } if (EnableVerboseLogging) @@ -194,6 +209,44 @@ public static IServiceCollection AddWebServices( return null; } } + + /// + /// Helper method to check if database is encrypted + /// + private static bool IsDatabaseEncrypted(string connectionString) + { + var builder = new SqliteConnectionStringBuilder(connectionString); + var dbPath = builder.DataSource; + + if (!File.Exists(dbPath)) return false; + + try + { + using var conn = new SqliteConnection(connectionString); + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master;"; + cmd.ExecuteScalar(); + return false; // Opened successfully = not encrypted + } + catch (SqliteException ex) when (ex.SqliteErrorCode == 26) + { + return true; // Error 26 = encrypted + } + catch + { + return false; // Other errors = assume not encrypted + } + } + + /// + /// Helper method to extract database path from connection string + /// + private static string ExtractDatabasePath(string connectionString) + { + var builder = new SqliteConnectionStringBuilder(connectionString); + return builder.DataSource; + } } /// diff --git a/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor index 5089f57..86d21ce 100644 --- a/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor +++ b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor @@ -2,6 +2,7 @@ @using Aquiis.Core.Entities @using Aquiis.Application.Services +@using Aquiis.Application.Services.Workflows @using Aquiis.Infrastructure.Data @using Aquiis.SimpleStart.Shared.Services @using Aquiis.SimpleStart.Shared.Components.Account @@ -16,6 +17,7 @@ @inject UserContextService UserContext @inject NavigationManager Navigation @inject ToastService ToastService +@inject SampleDataWorkflowService SampleDataWorkflowService @attribute [OrganizationAuthorize("Owner", "Administrator")] @rendermode InteractiveServer @@ -50,7 +52,12 @@ IsOwner="@isOwner" IsAdministrator="@isAdministrator" IsCurrentOrganization="@isCurrentOrganization" - OnManageUsers="NavigateToManageUsers" OnEdit="NavigateToEdit" /> + IsGeneratingSample="@isGeneratingSample" + IsRemovingSample="@isRemovingSample" + OnManageUsers="NavigateToManageUsers" + OnEdit="NavigateToEdit" + OnGenerateSampleData="GenerateSampleData" + OnRemoveSampleData="RemoveSampleData" /> }
@@ -65,6 +72,8 @@ private string currentUserRole = string.Empty; private bool isOwner = false; private bool isAdministrator = false; + private bool isGeneratingSample = false; + private bool isRemovingSample = false; private bool isCurrentOrganization = false; @@ -197,4 +206,87 @@ { Navigation.NavigateTo("/administration/organizations"); } + + private async Task GenerateSampleData() + { + if (!await ConfirmSampleDataGeneration()) + return; + + isGeneratingSample = true; + StateHasChanged(); + + try + { + var result = await SampleDataWorkflowService.GenerateSampleDataAsync(); + + if (result.Success) + { + ToastService.ShowSuccess($"Sample data created successfully! {result.Message}"); + await Task.Delay(2000); // Brief delay to show success message + Navigation.NavigateTo("/propertymanagement/properties"); + } + else + { + ToastService.ShowError($"Failed to generate sample data: {result.Message}"); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error generating sample data: {ex.Message}"); + } + finally + { + isGeneratingSample = false; + StateHasChanged(); + } + } + + private async Task ConfirmSampleDataGeneration() + { + // Simple confirmation - in production you might want a modal dialog + return true; // For now, proceed without confirmation + // TODO: Add confirmation modal in future enhancement + } + + private async Task RemoveSampleData() + { + if (!await ConfirmSampleDataRemoval()) + return; + + isRemovingSample = true; + StateHasChanged(); + + try + { + var result = await SampleDataWorkflowService.RemoveSampleDataAsync(); + + if (result.Success) + { + ToastService.ShowSuccess($"Sample data removed successfully! {result.Message}"); + await Task.Delay(1000); // Brief delay to show success message + // Reload the page to reflect changes + Navigation.NavigateTo($"/administration/organizations/view/{Id}", forceLoad: true); + } + else + { + ToastService.ShowError($"Failed to remove sample data: {result.Message}"); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error removing sample data: {ex.Message}"); + } + finally + { + isRemovingSample = false; + StateHasChanged(); + } + } + + private async Task ConfirmSampleDataRemoval() + { + // Simple confirmation - in production you might want a modal dialog + return true; // For now, proceed without confirmation + // TODO: Add confirmation modal: "Are you sure? This will delete all sample data." + } } diff --git a/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabasePreview.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabasePreview.razor new file mode 100644 index 0000000..a55645f --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabasePreview.razor @@ -0,0 +1,443 @@ +@page "/administration/database/preview/{BackupFileName}" +@using Aquiis.Application.Models.DTOs +@using Aquiis.Application.Services +@inject DatabasePreviewService PreviewService +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +
+ @if (needsPassword) + { + +
+
+
+
+ Encrypted Database +
+
+

+ This database is encrypted. Enter the password to preview its contents. +

+ + @if (!string.IsNullOrEmpty(errorMessage)) + { +
+ @errorMessage +
+ } + +
+ + +
+ Password is used for this preview session only (not saved) +
+
+ +
+ + +
+
+
+
+
+ } + else if (isLoading) + { + +
+
+ Loading... +
+

Loading database preview...

+
+ } + else if (isUnlocked && previewData != null) + { + +
+
+

Database Preview

+

@DecodedFileName

+
+ + Read-Only Mode - This is a preview. No changes will be made to your active database. + @if (!string.IsNullOrEmpty(sessionPassword)) + { + + Using temporary session password (not saved to keychain) + + } +
+
+
+ +
+
+ + +
+
+
+
+ +

@previewData.PropertyCount

+

Properties

+
+
+
+
+
+
+ +

@previewData.TenantCount

+

Tenants

+
+
+
+
+
+
+ +

@previewData.LeaseCount

+

Leases

+
+
+
+
+
+
+ +

@previewData.InvoiceCount

+

Invoices

+
+
+
+
+ + +
+
+ +
+
+
+ + @if (activeTab == "properties") + { +
+ @if (previewData.Properties?.Any() == true) + { +
+ + + + + + + + + + + + @foreach (var property in previewData.Properties) + { + + + + + + + + } + +
AddressCityStateTypeStatus
@property.Address@property.City@property.State@property.PropertyType + @property.Status +
+
+ } + else + { +

No properties found

+ } +
+ } + + + @if (activeTab == "tenants") + { +
+ @if (previewData.Tenants?.Any() == true) + { +
+ + + + + + + + + + + @foreach (var tenant in previewData.Tenants) + { + + + + + + + } + +
NameEmailPhoneCreated
@tenant.FullName@tenant.Email@tenant.Phone@tenant.CreatedOn.ToString("d")
+
+ } + else + { +

No tenants found

+ } +
+ } + + + @if (activeTab == "leases") + { +
+ @if (previewData.Leases?.Any() == true) + { +
+ + + + + + + + + + + + + @foreach (var lease in previewData.Leases) + { + + + + + + + + + } + +
PropertyTenantStart DateEnd DateMonthly RentStatus
@lease.PropertyAddress@lease.TenantName@lease.StartDate.ToString("d")@lease.EndDate.ToString("d")@lease.MonthlyRent.ToString("C") + + @lease.Status + +
+
+ } + else + { +

No leases found

+ } +
+ } +
+
+
+ + +
+ + Coming Soon: Selective data import feature will allow you to import specific properties, tenants, or leases from this backup into your active database. +
+ } +
+ +@code { + [Parameter] + public string BackupFileName { get; set; } = string.Empty; + + private string DecodedFileName => Uri.UnescapeDataString(BackupFileName); + private string activeTab = "properties"; // Track active tab state + + private bool needsPassword = false; + private bool isUnlocking = false; + private bool isLoading = false; + private bool isUnlocked = false; + private string enteredPassword = string.Empty; + private string? errorMessage; + private string? sessionPassword; // Temporary password not saved to keychain + + private DatabasePreviewData? previewData; + + protected override async Task OnInitializedAsync() + { + isLoading = true; + + try + { + // Check if database is encrypted + needsPassword = await PreviewService.IsDatabaseEncryptedAsync(DecodedFileName); + + if (!needsPassword) + { + // Load preview data immediately for unencrypted databases + await LoadPreviewData(password: null); + } + else + { + // For encrypted databases, try keychain password first + var keychainPassword = await PreviewService.TryGetKeychainPasswordAsync(); + if (!string.IsNullOrEmpty(keychainPassword)) + { + var result = await PreviewService.VerifyPasswordAsync(DecodedFileName, keychainPassword); + if (result.Success) + { + // Keychain password works + await LoadPreviewData(keychainPassword); + needsPassword = false; + } + // else: Wrong password, show prompt + } + } + } + catch (Exception ex) + { + errorMessage = $"Error loading database: {ex.Message}"; + } + finally + { + isLoading = false; + } + } + + private async Task UnlockDatabase() + { + if (string.IsNullOrWhiteSpace(enteredPassword)) + { + errorMessage = "Please enter a password."; + return; + } + + isUnlocking = true; + errorMessage = null; + + try + { + var result = await PreviewService.VerifyPasswordAsync(DecodedFileName, enteredPassword); + + if (result.Success) + { + // Store as session password (never save to keychain for preview) + sessionPassword = enteredPassword; + + await LoadPreviewData(enteredPassword); + isUnlocked = true; + needsPassword = false; + } + else + { + errorMessage = "Incorrect password. Please try again."; + } + } + catch (Exception ex) + { + errorMessage = $"Error unlocking database: {ex.Message}"; + } + finally + { + isUnlocking = false; + } + } + + private async Task LoadPreviewData(string? password) + { + isLoading = true; + + try + { + previewData = await PreviewService.GetPreviewDataAsync(DecodedFileName, password); + isUnlocked = true; + } + catch (Exception ex) + { + errorMessage = $"Error loading preview data: {ex.Message}"; + } + finally + { + isLoading = false; + } + } + + private void BackToSettings() + { + Navigation.NavigateTo("/administration/settings/database"); + } + + private async Task HandlePasswordKeyPress(KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + await UnlockDatabase(); + } + } + + private string GetLeaseStatusClass(string status) + { + return status switch + { + "Active" => "bg-success", + "Expired" => "bg-danger", + "Terminated" => "bg-warning", + _ => "bg-secondary" + }; + } + + private void SwitchTab(string tab) + { + activeTab = tab; + } +} diff --git a/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabaseSettings.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabaseSettings.razor index 26fb78b..a0e1b7f 100644 --- a/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabaseSettings.razor +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/DatabaseSettings.razor @@ -309,6 +309,10 @@ disabled="@isDownloading"> Download + +
+
+ This is the password you used when encrypting the database. +
+
+ + + + +
+ + + Your password is stored securely in the system keychain after successful unlock. + +
+ } + else + { + +
+ +
+ +

Database Unlocked!

+

+ Your password has been verified and stored securely. +

+ + + + + +
+ + The application will restart to load your database. + +
+ } +
+ + + + +@code { + private string password = string.Empty; + private string? errorMessage; + private bool isUnlocking = false; + private bool showPassword = false; + private bool unlockSuccessful = false; + private int countdownSeconds = 5; + private System.Threading.Timer? countdownTimer; + + private async Task HandleUnlock() + { + errorMessage = null; + isUnlocking = true; + + try + { + var (success, error) = await UnlockService.UnlockDatabaseAsync( + UnlockState.ConnectionString!, + password); + + if (success) + { + // Mark unlock complete + UnlockState.NeedsUnlock = false; + UnlockState.NotifyUnlockSuccess(); + + // Show success message with countdown + unlockSuccessful = true; + StartCountdown(); + } + else + { + errorMessage = error; + password = string.Empty; // Clear password on failure + } + } + catch (Exception ex) + { + errorMessage = $"An unexpected error occurred: {ex.Message}"; + } + finally + { + isUnlocking = false; + } + } + + private void StartCountdown() + { + countdownTimer = new System.Threading.Timer(_ => + { + countdownSeconds--; + InvokeAsync(StateHasChanged); + + if (countdownSeconds <= 0) + { + countdownTimer?.Dispose(); + Navigation.NavigateTo("/", forceLoad: true); + } + }, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); + } + + private void RestartNow() + { + countdownTimer?.Dispose(); + Navigation.NavigateTo("/", forceLoad: true); + } + + public void Dispose() + { + countdownTimer?.Dispose(); + } +} diff --git a/4-Aquiis.SimpleStart/Program.cs b/4-Aquiis.SimpleStart/Program.cs index b34b3c1..a3ae940 100644 --- a/4-Aquiis.SimpleStart/Program.cs +++ b/4-Aquiis.SimpleStart/Program.cs @@ -7,6 +7,7 @@ using Aquiis.Core.Interfaces; using Aquiis.Core.Interfaces.Services; using Aquiis.SimpleStart.Extensions; +using Aquiis.Infrastructure.Services; using Aquiis.SimpleStart.Shared.Services; using Aquiis.SimpleStart.Shared.Authorization; using Aquiis.Application.Services; @@ -17,7 +18,6 @@ using Microsoft.Extensions.Options; using Aquiis.Application.Services.PdfGenerators; using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.Infrastructure.Services; // Initialize SQLCipher before any database operations SQLitePCL.Batteries_V2.Init(); @@ -189,6 +189,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Database unlock service (always available, even when database locked) +builder.Services.AddScoped(); + // Configure and register session timeout service builder.Services.AddScoped(sp => { @@ -222,7 +225,15 @@ // Ensure database is created and migrations are applied using (var scope = app.Services.CreateScope()) { - // Get services + // Check if database is locked + var unlockState = scope.ServiceProvider.GetService(); + if (unlockState?.NeedsUnlock == true) + { + app.Logger.LogWarning("Database locked - skipping migrations and seeding. User will be prompted to unlock."); + } + else + { + // Normal database initialization flow var dbService = scope.ServiceProvider.GetRequiredService(); var identityContext = scope.ServiceProvider.GetRequiredService(); var backupService = scope.ServiceProvider.GetRequiredService(); @@ -542,6 +553,7 @@ { app.Logger.LogInformation("Schema version validated: {Version}", currentDbVersion); } + } // End of else block for database initialization when not locked } // Configure the HTTP request pipeline. @@ -616,42 +628,51 @@ }).RequireAuthorization(); // Create system service account for background jobs -// Clear connection pool to ensure all connections use proper encryption interceptor -Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); - +// Skip if database is locked using (var scope = app.Services.CreateScope()) { - var userManager = scope.ServiceProvider.GetRequiredService>(); - - var systemUser = await userManager.FindByIdAsync(ApplicationConstants.SystemUser.Id); - if (systemUser == null) + var unlockState = scope.ServiceProvider.GetService(); + if (unlockState?.NeedsUnlock == true) { - systemUser = new ApplicationUser - { - Id = ApplicationConstants.SystemUser.Id, - UserName = ApplicationConstants.SystemUser.Email, // UserName = Email in this system - NormalizedUserName = ApplicationConstants.SystemUser.Email.ToUpperInvariant(), - Email = ApplicationConstants.SystemUser.Email, - NormalizedEmail = ApplicationConstants.SystemUser.Email.ToUpperInvariant(), - EmailConfirmed = true, - FirstName = ApplicationConstants.SystemUser.FirstName, - LastName = ApplicationConstants.SystemUser.LastName, - LockoutEnabled = true, // CRITICAL: Account is locked by default - LockoutEnd = DateTimeOffset.MaxValue, // Locked until end of time - AccessFailedCount = 0 - }; - - // Create without password - cannot be used for login - var result = await userManager.CreateAsync(systemUser); + app.Logger.LogWarning("Database locked - skipping system user creation. Will be created after unlock."); + } + else + { + // Clear connection pool to ensure all connections use proper encryption interceptor + Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); + + var userManager = scope.ServiceProvider.GetRequiredService>(); - if (!result.Succeeded) + var systemUser = await userManager.FindByIdAsync(ApplicationConstants.SystemUser.Id); + if (systemUser == null) { - throw new Exception($"Failed to create system user: {string.Join(", ", result.Errors.Select(e => e.Description))}"); + systemUser = new ApplicationUser + { + Id = ApplicationConstants.SystemUser.Id, + UserName = ApplicationConstants.SystemUser.Email, // UserName = Email in this system + NormalizedUserName = ApplicationConstants.SystemUser.Email.ToUpperInvariant(), + Email = ApplicationConstants.SystemUser.Email, + NormalizedEmail = ApplicationConstants.SystemUser.Email.ToUpperInvariant(), + EmailConfirmed = true, + FirstName = ApplicationConstants.SystemUser.FirstName, + LastName = ApplicationConstants.SystemUser.LastName, + LockoutEnabled = true, // CRITICAL: Account is locked by default + LockoutEnd = DateTimeOffset.MaxValue, // Locked until end of time + AccessFailedCount = 0 + }; + + // Create without password - cannot be used for login + var result = await userManager.CreateAsync(systemUser); + + if (!result.Succeeded) + { + throw new Exception($"Failed to create system user: {string.Join(", ", result.Errors.Select(e => e.Description))}"); + } + + // DO NOT assign to any organization - service account is org-agnostic + // DO NOT create OrganizationUsers entries + // DO NOT set ActiveOrganizationId } - - // DO NOT assign to any organization - service account is org-agnostic - // DO NOT create OrganizationUsers entries - // DO NOT set ActiveOrganizationId } } diff --git a/4-Aquiis.SimpleStart/Properties/launchSettings.json b/4-Aquiis.SimpleStart/Properties/launchSettings.json index 284a318..27553db 100644 --- a/4-Aquiis.SimpleStart/Properties/launchSettings.json +++ b/4-Aquiis.SimpleStart/Properties/launchSettings.json @@ -1,23 +1,33 @@ { "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:5197", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:7087;http://localhost:5197", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5197", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7087;http://localhost:5197", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "Electron": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:8888", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "commandLineArgs": "/electronPort=8888 /watch" } } +} diff --git a/4-Aquiis.SimpleStart/Shared/Routes.razor b/4-Aquiis.SimpleStart/Shared/Routes.razor index 676a938..d30063d 100644 --- a/4-Aquiis.SimpleStart/Shared/Routes.razor +++ b/4-Aquiis.SimpleStart/Shared/Routes.razor @@ -1,11 +1,23 @@ @using Aquiis.SimpleStart.Shared.Components.Account.Shared - - - - - - - - - - +@using Aquiis.Infrastructure.Services +@inject DatabaseUnlockState? UnlockState + +@if (UnlockState?.NeedsUnlock == true) +{ + + +} +else +{ + + + + + + + + + + + +} diff --git a/4-Aquiis.SimpleStart/electron.manifest.json b/4-Aquiis.SimpleStart/electron.manifest.json index fe964f3..f5725b8 100644 --- a/4-Aquiis.SimpleStart/electron.manifest.json +++ b/4-Aquiis.SimpleStart/electron.manifest.json @@ -17,7 +17,7 @@ "appId": "com.aquiis.propertymanagement", "productName": "Aquiis", "copyright": "Copyright © 2026", - "buildVersion": "1.0.1", + "buildVersion": "1.1.0", "compression": "normal", "directories": { "output": "../../../bin/Desktop" @@ -44,7 +44,7 @@ }, "win": { "target": "portable", - "icon": "bin/Assets/icon.ico", + "icon": "Assets/icon.ico", "signAndEditExecutable": false, "artifactName": "${productName}-${version}-${arch}.${ext}" }, diff --git a/4-Aquiis.SimpleStart/wwwroot/js/fileDownload.js b/4-Aquiis.SimpleStart/wwwroot/js/fileDownload.js index f4be8a0..4cd56c2 100644 --- a/4-Aquiis.SimpleStart/wwwroot/js/fileDownload.js +++ b/4-Aquiis.SimpleStart/wwwroot/js/fileDownload.js @@ -72,3 +72,20 @@ window.viewFile = function (base64Data, mimeType) { alert("Error viewing file. Please try again."); } }; + +/** + * Clicks an element by its ID (useful for triggering hidden file inputs) + * @param {string} elementId - The ID of the element to click + */ +window.clickElementById = function (elementId) { + try { + const element = document.getElementById(elementId); + if (element) { + element.click(); + } else { + console.error(`Element with ID '${elementId}' not found`); + } + } catch (error) { + console.error("Error clicking element:", error); + } +}; diff --git a/6-Tests/Aquiis.Application.Tests/Services/InvoiceServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/InvoiceServiceTests.cs index 7687ae4..7eaa34d 100644 --- a/6-Tests/Aquiis.Application.Tests/Services/InvoiceServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/InvoiceServiceTests.cs @@ -554,8 +554,24 @@ public async Task GetInvoiceWithRelationsAsync_LoadsAllRelations() [Fact] public async Task GenerateInvoiceNumberAsync_GeneratesUniqueNumber() { - // Act + // Act - Generate first number and create invoice to persist it var invoiceNumber1 = await _service.GenerateInvoiceNumberAsync(); + var invoice1 = new Invoice + { + OrganizationId = _testOrgId, + LeaseId = _testLeaseId, + InvoiceNumber = invoiceNumber1, + InvoicedOn = DateTime.Today, + DueOn = DateTime.Today.AddDays(30), + Amount = 1500, + Description = "Test Invoice 1", + Status = "Pending", + CreatedBy = _testUserId, + CreatedOn = DateTime.UtcNow + }; + await _service.CreateAsync(invoice1); + + // Generate second number - should see the first invoice and increment var invoiceNumber2 = await _service.GenerateInvoiceNumberAsync(); // Assert @@ -563,8 +579,22 @@ public async Task GenerateInvoiceNumberAsync_GeneratesUniqueNumber() Assert.NotNull(invoiceNumber2); Assert.StartsWith("INV-", invoiceNumber1); Assert.StartsWith("INV-", invoiceNumber2); - // Numbers should be same format but potentially different sequence - Assert.Matches(@"^INV-\d{6}-\d{5}$", invoiceNumber1); + + // Verify correct format: INV-{YYYYMM}-{0001} + Assert.Matches(@"^INV-\d{6}-\d{4}$", invoiceNumber1); + Assert.Matches(@"^INV-\d{6}-\d{4}$", invoiceNumber2); + + // Verify both numbers are unique + Assert.NotEqual(invoiceNumber1, invoiceNumber2); + + // Verify sequential numbering (both must be in same month for this test) + var parts1 = invoiceNumber1.Split('-'); + var parts2 = invoiceNumber2.Split('-'); + Assert.Equal(parts1[1], parts2[1]); // Same month (test runs fast enough) + + var seq1 = int.Parse(parts1[2]); + var seq2 = int.Parse(parts2[2]); + Assert.Equal(seq1 + 1, seq2); // Should increment: 0001 -> 0002 } [Fact] From c6903144483170b5d6db49892a9fdc426cdfb0e3 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Mon, 16 Feb 2026 19:52:59 -0600 Subject: [PATCH 08/10] security-enhancements --- .../Entities/ApplicationScreening.cs | 4 - 0-Aquiis.Core/Entities/BaseModel.cs | 14 + 0-Aquiis.Core/Entities/CalendarEvent.cs | 4 - 0-Aquiis.Core/Entities/CalendarSettings.cs | 3 - 0-Aquiis.Core/Entities/Checklist.cs | 4 - 0-Aquiis.Core/Entities/ChecklistItem.cs | 5 - 0-Aquiis.Core/Entities/ChecklistTemplate.cs | 4 - .../Entities/ChecklistTemplateItem.cs | 4 - 0-Aquiis.Core/Entities/Document.cs | 6 - 0-Aquiis.Core/Entities/Inspection.cs | 5 - 0-Aquiis.Core/Entities/Invoice.cs | 6 - 0-Aquiis.Core/Entities/Lease.cs | 4 - 0-Aquiis.Core/Entities/LeaseOffer.cs | 5 - 0-Aquiis.Core/Entities/MaintenanceRequest.cs | 4 - 0-Aquiis.Core/Entities/Note.cs | 5 - 0-Aquiis.Core/Entities/Notification.cs | 3 - .../Entities/NotificationPreferences.cs | 3 - .../Entities/OrganizationEmailSettings.cs | 3 - .../Entities/OrganizationSMSSettings.cs | 3 - .../Entities/OrganizationSettings.cs | 6 - 0-Aquiis.Core/Entities/Payment.cs | 5 - 0-Aquiis.Core/Entities/Property.cs | 7 - 0-Aquiis.Core/Entities/ProspectiveTenant.cs | 5 - 0-Aquiis.Core/Entities/RentalApplication.cs | 7 - 0-Aquiis.Core/Entities/Repair.cs | 4 - 0-Aquiis.Core/Entities/SecurityDeposit.cs | 6 - .../Entities/SecurityDepositDividend.cs | 6 - .../Entities/SecurityDepositInvestmentPool.cs | 3 - 0-Aquiis.Core/Entities/Tenant.cs | 4 - 0-Aquiis.Core/Entities/Tour.cs | 4 - 0-Aquiis.Core/Entities/UserProfile.cs | 3 +- 0-Aquiis.Core/Entities/WorkflowAuditLog.cs | 5 - ...0212163628_AddIsSampleDataFlag.Designer.cs | 4404 +++++++++++++++++ .../20260212163628_AddIsSampleDataFlag.cs | 660 +++ ...7_UpdateExistingSampleDataFlag.Designer.cs | 4404 +++++++++++++++++ ...0212165047_UpdateExistingSampleDataFlag.cs | 65 + ...idateOrganizationIdToBaseModel.Designer.cs | 4392 ++++++++++++++++ ...19_ConsolidateOrganizationIdToBaseModel.cs | 28 + .../ApplicationDbContextModelSnapshot.cs | 152 +- .../Hubs/NotificationHub.cs | 47 + .../Services/NotificationService.cs | 176 + .../Workflows/SampleDataWorkflowService.cs | 246 +- 3-Aquiis.UI.Shared/Aquiis.UI.Shared.csproj | 1 + .../Organizations/OrganizationCard.razor | 38 +- .../Entities/Properties/PropertyCard.razor | 17 +- .../Entities/Properties/PropertyDetails.razor | 15 +- .../Entities/Properties/PropertyFormModel.cs | 2 + .../Entities/Properties/PropertyList.razor | 6 + .../Properties/PropertyListView.razor | 1 + .../Notifications/NotificationBell.razor | 133 +- .../Notifications/NotificationCenter.razor | 214 +- .../Organizations/OrganizationDetails.razor | 11 +- .../Properties/PropertyEditForm.razor | 4 +- .../Properties/PropertyViewForm.razor | 14 +- 3-Aquiis.UI.Shared/_Imports.razor | 1 + .../Pages/ViewOrganization.razor | 28 +- .../Features/Calendar/CalendarListView.razor | 256 +- 4-Aquiis.SimpleStart/Program.cs | 6 + .../Features/Calendar/CalendarListView.razor | 256 +- 5-Aquiis.Professional/Program.cs | 6 + .../Services/LeaseWorkflowService.Tests.cs | 3 + .../Services/NotificationServiceTests.cs | 3 + .../Services/PaymentServiceTests.cs | 3 + .../Services/PropertyServiceTests.cs | 3 + ...pplicationWorkflowService.EdgeCaseTests.cs | 3 + ...tionWorkflowService.LeaseLifecycleTests.cs | 3 + .../ApplicationWorkflowServiceTests.cs | 3 + 67 files changed, 15506 insertions(+), 252 deletions(-) create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260212163628_AddIsSampleDataFlag.Designer.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260212163628_AddIsSampleDataFlag.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260212165047_UpdateExistingSampleDataFlag.Designer.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260212165047_UpdateExistingSampleDataFlag.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260216205819_ConsolidateOrganizationIdToBaseModel.Designer.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260216205819_ConsolidateOrganizationIdToBaseModel.cs create mode 100644 1-Aquiis.Infrastructure/Hubs/NotificationHub.cs diff --git a/0-Aquiis.Core/Entities/ApplicationScreening.cs b/0-Aquiis.Core/Entities/ApplicationScreening.cs index 1c4a702..f5cf216 100644 --- a/0-Aquiis.Core/Entities/ApplicationScreening.cs +++ b/0-Aquiis.Core/Entities/ApplicationScreening.cs @@ -6,10 +6,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/BaseModel.cs b/0-Aquiis.Core/Entities/BaseModel.cs index 815be2f..daa93f9 100644 --- a/0-Aquiis.Core/Entities/BaseModel.cs +++ b/0-Aquiis.Core/Entities/BaseModel.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; using Aquiis.Core.Interfaces; +using Aquiis.Core.Validation; namespace Aquiis.Core.Entities { @@ -12,6 +13,15 @@ public class BaseModel : IAuditable [DatabaseGenerated(DatabaseGeneratedOption.None)] public Guid Id { get; set; } + /// + /// Organization partition key - all entities are scoped to an organization for multi-tenancy. + /// This is the fundamental isolation boundary in the system. + /// + [RequiredGuid] + [JsonInclude] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + [Required] [JsonInclude] [DataType(DataType.DateTime)] @@ -39,5 +49,9 @@ public class BaseModel : IAuditable [JsonInclude] [Display(Name = "Is Deleted?")] public bool IsDeleted { get; set; } = false; + + [JsonInclude] + [Display(Name = "Is Sample Data?")] + public bool IsSampleData { 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 index b1c820c..8cc02ba 100644 --- a/0-Aquiis.Core/Entities/CalendarEvent.cs +++ b/0-Aquiis.Core/Entities/CalendarEvent.cs @@ -10,10 +10,6 @@ namespace Aquiis.Core.Entities /// public class CalendarEvent : BaseModel { - [RequiredGuid] - [Display(Name = "Organization ID")] - public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] [StringLength(200)] [Display(Name = "Title")] diff --git a/0-Aquiis.Core/Entities/CalendarSettings.cs b/0-Aquiis.Core/Entities/CalendarSettings.cs index 01d76d4..e3e3407 100644 --- a/0-Aquiis.Core/Entities/CalendarSettings.cs +++ b/0-Aquiis.Core/Entities/CalendarSettings.cs @@ -5,9 +5,6 @@ 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; diff --git a/0-Aquiis.Core/Entities/Checklist.cs b/0-Aquiis.Core/Entities/Checklist.cs index ae23e63..7146206 100644 --- a/0-Aquiis.Core/Entities/Checklist.cs +++ b/0-Aquiis.Core/Entities/Checklist.cs @@ -6,10 +6,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/ChecklistItem.cs b/0-Aquiis.Core/Entities/ChecklistItem.cs index fbae6bc..a3e6053 100644 --- a/0-Aquiis.Core/Entities/ChecklistItem.cs +++ b/0-Aquiis.Core/Entities/ChecklistItem.cs @@ -6,11 +6,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/ChecklistTemplate.cs b/0-Aquiis.Core/Entities/ChecklistTemplate.cs index d60e781..98d01e4 100644 --- a/0-Aquiis.Core/Entities/ChecklistTemplate.cs +++ b/0-Aquiis.Core/Entities/ChecklistTemplate.cs @@ -5,10 +5,6 @@ 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")] diff --git a/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs b/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs index 6c06555..1ac8515 100644 --- a/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs +++ b/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs @@ -6,10 +6,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/Document.cs b/0-Aquiis.Core/Entities/Document.cs index 4439541..9beb226 100644 --- a/0-Aquiis.Core/Entities/Document.cs +++ b/0-Aquiis.Core/Entities/Document.cs @@ -5,12 +5,6 @@ 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; diff --git a/0-Aquiis.Core/Entities/Inspection.cs b/0-Aquiis.Core/Entities/Inspection.cs index 746d043..3787103 100644 --- a/0-Aquiis.Core/Entities/Inspection.cs +++ b/0-Aquiis.Core/Entities/Inspection.cs @@ -6,11 +6,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/Invoice.cs b/0-Aquiis.Core/Entities/Invoice.cs index ab7da0b..de923d6 100644 --- a/0-Aquiis.Core/Entities/Invoice.cs +++ b/0-Aquiis.Core/Entities/Invoice.cs @@ -5,12 +5,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/Lease.cs b/0-Aquiis.Core/Entities/Lease.cs index 3dc23f3..a4e352d 100644 --- a/0-Aquiis.Core/Entities/Lease.cs +++ b/0-Aquiis.Core/Entities/Lease.cs @@ -7,10 +7,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/LeaseOffer.cs b/0-Aquiis.Core/Entities/LeaseOffer.cs index 05046eb..e7f73d9 100644 --- a/0-Aquiis.Core/Entities/LeaseOffer.cs +++ b/0-Aquiis.Core/Entities/LeaseOffer.cs @@ -5,11 +5,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/MaintenanceRequest.cs b/0-Aquiis.Core/Entities/MaintenanceRequest.cs index 6b0cca9..67cfd66 100644 --- a/0-Aquiis.Core/Entities/MaintenanceRequest.cs +++ b/0-Aquiis.Core/Entities/MaintenanceRequest.cs @@ -6,10 +6,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/Note.cs b/0-Aquiis.Core/Entities/Note.cs index 0575a23..0b7091b 100644 --- a/0-Aquiis.Core/Entities/Note.cs +++ b/0-Aquiis.Core/Entities/Note.cs @@ -8,11 +8,6 @@ namespace Aquiis.Core.Entities /// public class Note : BaseModel { - [Required] - [StringLength(100)] - [Display(Name = "Organization ID")] - public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] [StringLength(5000)] [Display(Name = "Content")] diff --git a/0-Aquiis.Core/Entities/Notification.cs b/0-Aquiis.Core/Entities/Notification.cs index f24b994..e730093 100644 --- a/0-Aquiis.Core/Entities/Notification.cs +++ b/0-Aquiis.Core/Entities/Notification.cs @@ -5,9 +5,6 @@ public class Notification : BaseModel { - [RequiredGuid] - public Guid OrganizationId { get; set; } - [Required] [StringLength(200)] public string Title { get; set; } = string.Empty; diff --git a/0-Aquiis.Core/Entities/NotificationPreferences.cs b/0-Aquiis.Core/Entities/NotificationPreferences.cs index 2dd9626..b5d68cd 100644 --- a/0-Aquiis.Core/Entities/NotificationPreferences.cs +++ b/0-Aquiis.Core/Entities/NotificationPreferences.cs @@ -5,9 +5,6 @@ namespace Aquiis.Core.Entities; public class NotificationPreferences : BaseModel { - [RequiredGuid] - public Guid OrganizationId { get; set; } - [Required] public string UserId { get; set; } = string.Empty; diff --git a/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs b/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs index 6188ffc..5790175 100644 --- a/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs +++ b/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs @@ -11,9 +11,6 @@ namespace Aquiis.Core.Entities /// public class OrganizationEmailSettings : BaseModel { - [RequiredGuid] - public Guid OrganizationId { get; set; } - public string ProviderName { get; set; } = "SMTP"; public string SmtpServer { get; set; } = string.Empty; diff --git a/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs b/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs index 896fed1..96cfc79 100644 --- a/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs +++ b/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs @@ -11,9 +11,6 @@ namespace Aquiis.Core.Entities /// public class OrganizationSMSSettings : BaseModel { - [RequiredGuid] - public Guid OrganizationId { get; set; } - // Twilio Configuration public bool IsSMSEnabled { get; set; } diff --git a/0-Aquiis.Core/Entities/OrganizationSettings.cs b/0-Aquiis.Core/Entities/OrganizationSettings.cs index 94f2945..f6b9f77 100644 --- a/0-Aquiis.Core/Entities/OrganizationSettings.cs +++ b/0-Aquiis.Core/Entities/OrganizationSettings.cs @@ -10,12 +10,6 @@ namespace Aquiis.Core.Entities /// 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; } diff --git a/0-Aquiis.Core/Entities/Payment.cs b/0-Aquiis.Core/Entities/Payment.cs index 03e652d..93ada71 100644 --- a/0-Aquiis.Core/Entities/Payment.cs +++ b/0-Aquiis.Core/Entities/Payment.cs @@ -5,11 +5,6 @@ namespace Aquiis.Core.Entities { public class Payment : BaseModel { - [Required] - [StringLength(100)] - [Display(Name = "Organization ID")] - public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] [StringLength(50)] [Display(Name = "Payment Number")] diff --git a/0-Aquiis.Core/Entities/Property.cs b/0-Aquiis.Core/Entities/Property.cs index 4279a11..504abe9 100644 --- a/0-Aquiis.Core/Entities/Property.cs +++ b/0-Aquiis.Core/Entities/Property.cs @@ -7,13 +7,6 @@ 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)] diff --git a/0-Aquiis.Core/Entities/ProspectiveTenant.cs b/0-Aquiis.Core/Entities/ProspectiveTenant.cs index 2d72942..9e04ad6 100644 --- a/0-Aquiis.Core/Entities/ProspectiveTenant.cs +++ b/0-Aquiis.Core/Entities/ProspectiveTenant.cs @@ -5,11 +5,6 @@ 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")] diff --git a/0-Aquiis.Core/Entities/RentalApplication.cs b/0-Aquiis.Core/Entities/RentalApplication.cs index f7feb86..37392ea 100644 --- a/0-Aquiis.Core/Entities/RentalApplication.cs +++ b/0-Aquiis.Core/Entities/RentalApplication.cs @@ -6,13 +6,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/Repair.cs b/0-Aquiis.Core/Entities/Repair.cs index d6fd4dd..9f7fd63 100644 --- a/0-Aquiis.Core/Entities/Repair.cs +++ b/0-Aquiis.Core/Entities/Repair.cs @@ -13,10 +13,6 @@ namespace Aquiis.Core.Entities; public class Repair : BaseModel { // Core Identity - [RequiredGuid] - [Display(Name = "Organization ID")] - public Guid OrganizationId { get; set; } = Guid.Empty; - [RequiredGuid] public Guid PropertyId { get; set; } diff --git a/0-Aquiis.Core/Entities/SecurityDeposit.cs b/0-Aquiis.Core/Entities/SecurityDeposit.cs index 6e645eb..06a60ba 100644 --- a/0-Aquiis.Core/Entities/SecurityDeposit.cs +++ b/0-Aquiis.Core/Entities/SecurityDeposit.cs @@ -10,12 +10,6 @@ namespace Aquiis.Core.Entities /// 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; } diff --git a/0-Aquiis.Core/Entities/SecurityDepositDividend.cs b/0-Aquiis.Core/Entities/SecurityDepositDividend.cs index 47e418c..3c3a084 100644 --- a/0-Aquiis.Core/Entities/SecurityDepositDividend.cs +++ b/0-Aquiis.Core/Entities/SecurityDepositDividend.cs @@ -10,12 +10,6 @@ namespace Aquiis.Core.Entities /// 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; } diff --git a/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs b/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs index f5417cb..059612f 100644 --- a/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs +++ b/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs @@ -9,9 +9,6 @@ namespace Aquiis.Core.Entities /// public class SecurityDepositInvestmentPool : BaseModel { - [Required] - public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] public int Year { get; set; } diff --git a/0-Aquiis.Core/Entities/Tenant.cs b/0-Aquiis.Core/Entities/Tenant.cs index b71b70c..d29cff7 100644 --- a/0-Aquiis.Core/Entities/Tenant.cs +++ b/0-Aquiis.Core/Entities/Tenant.cs @@ -5,10 +5,6 @@ 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; diff --git a/0-Aquiis.Core/Entities/Tour.cs b/0-Aquiis.Core/Entities/Tour.cs index 948f72d..40f4b1e 100644 --- a/0-Aquiis.Core/Entities/Tour.cs +++ b/0-Aquiis.Core/Entities/Tour.cs @@ -6,10 +6,6 @@ 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; } diff --git a/0-Aquiis.Core/Entities/UserProfile.cs b/0-Aquiis.Core/Entities/UserProfile.cs index eb57330..0acf418 100644 --- a/0-Aquiis.Core/Entities/UserProfile.cs +++ b/0-Aquiis.Core/Entities/UserProfile.cs @@ -31,8 +31,9 @@ public class UserProfile : BaseModel /// /// User's "home" organization - their primary/default organization. + /// Shadows BaseModel.OrganizationId to keep it nullable (users can exist before org assignment). /// - public Guid? OrganizationId { get; set; } + public new Guid? OrganizationId { get; set; } /// /// Currently active organization the user is viewing/working with. diff --git a/0-Aquiis.Core/Entities/WorkflowAuditLog.cs b/0-Aquiis.Core/Entities/WorkflowAuditLog.cs index 0bf156f..fd1fd39 100644 --- a/0-Aquiis.Core/Entities/WorkflowAuditLog.cs +++ b/0-Aquiis.Core/Entities/WorkflowAuditLog.cs @@ -44,11 +44,6 @@ public class WorkflowAuditLog : BaseModel /// public required DateTime PerformedOn { get; set; } - /// - /// Organization context for the workflow action - /// - public required Guid OrganizationId { get; set; } - /// /// Additional context data (JSON serialized) /// diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260212163628_AddIsSampleDataFlag.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260212163628_AddIsSampleDataFlag.Designer.cs new file mode 100644 index 0000000..a93165c --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260212163628_AddIsSampleDataFlag.Designer.cs @@ -0,0 +1,4404 @@ +// +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.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260212163628_AddIsSampleDataFlag")] + partial class AddIsSampleDataFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + 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("IsSampleData") + .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("IsSampleData") + .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("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 11, + ItemText = "Showed water heater location", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 18, + ItemText = "Explained pet policy", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Document property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Inspect property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Prepare information packets", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.DatabaseSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DatabaseEncryptionEnabled") + .HasColumnType("INTEGER"); + + b.Property("EncryptionChangedOn") + .HasColumnType("TEXT"); + + b.Property("EncryptionSalt") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DatabaseSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId", "InvoiceNumber") + .IsUnique() + .HasDatabaseName("IX_Invoice_OrgId_InvoiceNumber"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyDigestTime") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmailApplicationStatusChange") + .HasColumnType("INTEGER"); + + b.Property("EmailInspectionScheduled") + .HasColumnType("INTEGER"); + + b.Property("EmailLeaseExpiring") + .HasColumnType("INTEGER"); + + b.Property("EmailMaintenanceUpdate") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentReceived") + .HasColumnType("INTEGER"); + + b.Property("EnableDailyDigest") + .HasColumnType("INTEGER"); + + b.Property("EnableEmailNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableInAppNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableSMSNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableWeeklyDigest") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SMSLeaseExpiringUrgent") + .HasColumnType("INTEGER"); + + b.Property("SMSMaintenanceEmergency") + .HasColumnType("INTEGER"); + + b.Property("SMSPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WeeklyDigestDay") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("NotificationPreferences"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("DailyLimit") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentToday") + .HasColumnType("INTEGER"); + + b.Property("EnableSsl") + .HasColumnType("INTEGER"); + + b.Property("FromEmail") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FromName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsEmailEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastEmailSentOn") + .HasColumnType("TEXT"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastErrorOn") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyLimit") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendGridApiKeyEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SmtpPort") + .HasColumnType("INTEGER"); + + b.Property("SmtpServer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationEmailSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountBalance") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("AccountType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CostPerSMS") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSMSEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastSMSSentOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SMSSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("SMSSentToday") + .HasColumnType("INTEGER"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("TwilioAccountSidEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioAuthTokenEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioPhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSMSSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationUsers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId", "PaymentNumber") + .IsUnique() + .HasDatabaseName("IX_Payment_OrgId_PaymentNumber"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("ContactId") + .HasColumnType("TEXT"); + + b.Property("ContactPerson") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorId") + .HasColumnType("TEXT"); + + b.Property("ContractorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Cost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MaintenanceRequestId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PartsReplaced") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RepairType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("WarrantyApplies") + .HasColumnType("INTEGER"); + + b.Property("WarrantyExpiresOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("LeaseId"); + + b.HasIndex("MaintenanceRequestId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RepairType"); + + b.ToTable("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActiveOrganizationId"); + + b.HasIndex("Email"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserProfiles"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EmailError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EmailSent") + .HasColumnType("INTEGER"); + + b.Property("EmailSentOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRead") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReadOn") + .HasColumnType("TEXT"); + + b.Property("RecipientUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RelatedEntityId") + .HasColumnType("TEXT"); + + b.Property("RelatedEntityType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SMSError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SMSSent") + .HasColumnType("INTEGER"); + + b.Property("SMSSentOn") + .HasColumnType("TEXT"); + + b.Property("SendEmail") + .HasColumnType("INTEGER"); + + b.Property("SendInApp") + .HasColumnType("INTEGER"); + + b.Property("SendSMS") + .HasColumnType("INTEGER"); + + b.Property("SentOn") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsRead"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("SentOn"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("MaintenanceRequests") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.MaintenanceRequest", "MaintenanceRequest") + .WithMany("Repairs") + .HasForeignKey("MaintenanceRequestId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Repairs") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("MaintenanceRequest"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + + b.Navigation("MaintenanceRequests"); + + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260212163628_AddIsSampleDataFlag.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260212163628_AddIsSampleDataFlag.cs new file mode 100644 index 0000000..66077c2 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260212163628_AddIsSampleDataFlag.cs @@ -0,0 +1,660 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Infrastructure.Migrations +{ + /// + public partial class AddIsSampleDataFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Payments_OrganizationId", + table: "Payments"); + + migrationBuilder.DropIndex( + name: "IX_Invoices_InvoiceNumber", + table: "Invoices"); + + migrationBuilder.DropIndex( + name: "IX_Invoices_OrganizationId", + table: "Invoices"); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "WorkflowAuditLogs", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "UserProfiles", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Tours", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Tenants", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "SecurityDeposits", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "SecurityDepositInvestmentPools", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "SecurityDepositDividends", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Repairs", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "RentalApplications", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "ProspectiveTenants", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Properties", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Payments", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "OrganizationSMSSettings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "OrganizationSettings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "OrganizationEmailSettings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Notifications", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "NotificationPreferences", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Notes", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "MaintenanceRequests", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Leases", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "LeaseOffers", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Invoices", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Inspections", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Documents", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "ChecklistTemplates", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "ChecklistTemplateItems", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "Checklists", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "ChecklistItems", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "CalendarSettings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "CalendarEvents", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSampleData", + table: "ApplicationScreenings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000001"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000002"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000003"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000004"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000005"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000006"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000007"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000008"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000009"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000010"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000011"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000012"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000013"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000014"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000015"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000016"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000017"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000018"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000019"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000020"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000021"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000022"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000023"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000024"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000025"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000026"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000027"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000028"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000029"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000030"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000031"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000032"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000001"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000002"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000003"), + column: "IsSampleData", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000004"), + column: "IsSampleData", + value: false); + + migrationBuilder.CreateIndex( + name: "IX_Payment_OrgId_PaymentNumber", + table: "Payments", + columns: new[] { "OrganizationId", "PaymentNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Invoice_OrgId_InvoiceNumber", + table: "Invoices", + columns: new[] { "OrganizationId", "InvoiceNumber" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Payment_OrgId_PaymentNumber", + table: "Payments"); + + migrationBuilder.DropIndex( + name: "IX_Invoice_OrgId_InvoiceNumber", + table: "Invoices"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "WorkflowAuditLogs"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "UserProfiles"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Tours"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Tenants"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "SecurityDeposits"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "SecurityDepositInvestmentPools"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "SecurityDepositDividends"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Repairs"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "RentalApplications"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "ProspectiveTenants"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Properties"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Payments"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "OrganizationSMSSettings"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "OrganizationSettings"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "OrganizationEmailSettings"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Notifications"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "NotificationPreferences"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Notes"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "MaintenanceRequests"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Leases"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "LeaseOffers"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Invoices"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Inspections"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "ChecklistTemplates"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "ChecklistTemplateItems"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "Checklists"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "ChecklistItems"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "CalendarSettings"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "CalendarEvents"); + + migrationBuilder.DropColumn( + name: "IsSampleData", + table: "ApplicationScreenings"); + + migrationBuilder.CreateIndex( + name: "IX_Payments_OrganizationId", + table: "Payments", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Invoices_InvoiceNumber", + table: "Invoices", + column: "InvoiceNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Invoices_OrganizationId", + table: "Invoices", + column: "OrganizationId"); + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260212165047_UpdateExistingSampleDataFlag.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260212165047_UpdateExistingSampleDataFlag.Designer.cs new file mode 100644 index 0000000..582d000 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260212165047_UpdateExistingSampleDataFlag.Designer.cs @@ -0,0 +1,4404 @@ +// +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.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260212165047_UpdateExistingSampleDataFlag")] + partial class UpdateExistingSampleDataFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + 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("IsSampleData") + .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("IsSampleData") + .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("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 11, + ItemText = "Showed water heater location", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 18, + ItemText = "Explained pet policy", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Document property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Inspect property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Prepare information packets", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.DatabaseSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DatabaseEncryptionEnabled") + .HasColumnType("INTEGER"); + + b.Property("EncryptionChangedOn") + .HasColumnType("TEXT"); + + b.Property("EncryptionSalt") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DatabaseSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId", "InvoiceNumber") + .IsUnique() + .HasDatabaseName("IX_Invoice_OrgId_InvoiceNumber"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyDigestTime") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmailApplicationStatusChange") + .HasColumnType("INTEGER"); + + b.Property("EmailInspectionScheduled") + .HasColumnType("INTEGER"); + + b.Property("EmailLeaseExpiring") + .HasColumnType("INTEGER"); + + b.Property("EmailMaintenanceUpdate") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentReceived") + .HasColumnType("INTEGER"); + + b.Property("EnableDailyDigest") + .HasColumnType("INTEGER"); + + b.Property("EnableEmailNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableInAppNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableSMSNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableWeeklyDigest") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SMSLeaseExpiringUrgent") + .HasColumnType("INTEGER"); + + b.Property("SMSMaintenanceEmergency") + .HasColumnType("INTEGER"); + + b.Property("SMSPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WeeklyDigestDay") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("NotificationPreferences"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("DailyLimit") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentToday") + .HasColumnType("INTEGER"); + + b.Property("EnableSsl") + .HasColumnType("INTEGER"); + + b.Property("FromEmail") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FromName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsEmailEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastEmailSentOn") + .HasColumnType("TEXT"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastErrorOn") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyLimit") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendGridApiKeyEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SmtpPort") + .HasColumnType("INTEGER"); + + b.Property("SmtpServer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationEmailSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountBalance") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("AccountType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CostPerSMS") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSMSEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastSMSSentOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SMSSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("SMSSentToday") + .HasColumnType("INTEGER"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("TwilioAccountSidEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioAuthTokenEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioPhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSMSSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationUsers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId", "PaymentNumber") + .IsUnique() + .HasDatabaseName("IX_Payment_OrgId_PaymentNumber"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("ContactId") + .HasColumnType("TEXT"); + + b.Property("ContactPerson") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorId") + .HasColumnType("TEXT"); + + b.Property("ContractorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Cost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MaintenanceRequestId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PartsReplaced") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RepairType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("WarrantyApplies") + .HasColumnType("INTEGER"); + + b.Property("WarrantyExpiresOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("LeaseId"); + + b.HasIndex("MaintenanceRequestId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RepairType"); + + b.ToTable("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActiveOrganizationId"); + + b.HasIndex("Email"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserProfiles"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EmailError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EmailSent") + .HasColumnType("INTEGER"); + + b.Property("EmailSentOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRead") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReadOn") + .HasColumnType("TEXT"); + + b.Property("RecipientUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RelatedEntityId") + .HasColumnType("TEXT"); + + b.Property("RelatedEntityType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SMSError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SMSSent") + .HasColumnType("INTEGER"); + + b.Property("SMSSentOn") + .HasColumnType("TEXT"); + + b.Property("SendEmail") + .HasColumnType("INTEGER"); + + b.Property("SendInApp") + .HasColumnType("INTEGER"); + + b.Property("SendSMS") + .HasColumnType("INTEGER"); + + b.Property("SentOn") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsRead"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("SentOn"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("MaintenanceRequests") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.MaintenanceRequest", "MaintenanceRequest") + .WithMany("Repairs") + .HasForeignKey("MaintenanceRequestId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Repairs") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("MaintenanceRequest"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + + b.Navigation("MaintenanceRequests"); + + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260212165047_UpdateExistingSampleDataFlag.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260212165047_UpdateExistingSampleDataFlag.cs new file mode 100644 index 0000000..9e6ad1a --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260212165047_UpdateExistingSampleDataFlag.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Infrastructure.Migrations +{ + /// + public partial class UpdateExistingSampleDataFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Set IsSampleData = true for all existing records created by SystemUser + // SystemUser.Id = '00000000-0000-0000-0000-000000000001' + var systemUserId = "00000000-0000-0000-0000-000000000001"; + + // Update Properties + migrationBuilder.Sql( + $"UPDATE Properties SET IsSampleData = 1 WHERE CreatedBy = '{systemUserId}';"); + + // Update Tenants + migrationBuilder.Sql( + $"UPDATE Tenants SET IsSampleData = 1 WHERE CreatedBy = '{systemUserId}';"); + + // Update Leases + migrationBuilder.Sql( + $"UPDATE Leases SET IsSampleData = 1 WHERE CreatedBy = '{systemUserId}';"); + + // Update Invoices + migrationBuilder.Sql( + $"UPDATE Invoices SET IsSampleData = 1 WHERE CreatedBy = '{systemUserId}';"); + + // Update Payments + migrationBuilder.Sql( + $"UPDATE Payments SET IsSampleData = 1 WHERE CreatedBy = '{systemUserId}';"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Reset IsSampleData = false for records that were marked as sample data + var systemUserId = "00000000-0000-0000-0000-000000000001"; + + // Reset Properties + migrationBuilder.Sql( + $"UPDATE Properties SET IsSampleData = 0 WHERE CreatedBy = '{systemUserId}';"); + + // Reset Tenants + migrationBuilder.Sql( + $"UPDATE Tenants SET IsSampleData = 0 WHERE CreatedBy = '{systemUserId}';"); + + // Reset Leases + migrationBuilder.Sql( + $"UPDATE Leases SET IsSampleData = 0 WHERE CreatedBy = '{systemUserId}';"); + + // Reset Invoices + migrationBuilder.Sql( + $"UPDATE Invoices SET IsSampleData = 0 WHERE CreatedBy = '{systemUserId}';"); + + // Reset Payments + migrationBuilder.Sql( + $"UPDATE Payments SET IsSampleData = 0 WHERE CreatedBy = '{systemUserId}';"); + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260216205819_ConsolidateOrganizationIdToBaseModel.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260216205819_ConsolidateOrganizationIdToBaseModel.Designer.cs new file mode 100644 index 0000000..add09ea --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260216205819_ConsolidateOrganizationIdToBaseModel.Designer.cs @@ -0,0 +1,4392 @@ +// +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.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260216205819_ConsolidateOrganizationIdToBaseModel")] + partial class ConsolidateOrganizationIdToBaseModel + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + 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("IsSampleData") + .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("IsSampleData") + .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("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSampleData = false, + IsSystemTemplate = true, + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 11, + ItemText = "Showed water heater location", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 18, + ItemText = "Explained pet policy", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Document property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Inspect property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 2, + ItemText = "Prepare information packets", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + IsSampleData = false, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.DatabaseSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DatabaseEncryptionEnabled") + .HasColumnType("INTEGER"); + + b.Property("EncryptionChangedOn") + .HasColumnType("TEXT"); + + b.Property("EncryptionSalt") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DatabaseSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId", "InvoiceNumber") + .IsUnique() + .HasDatabaseName("IX_Invoice_OrgId_InvoiceNumber"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyDigestTime") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmailApplicationStatusChange") + .HasColumnType("INTEGER"); + + b.Property("EmailInspectionScheduled") + .HasColumnType("INTEGER"); + + b.Property("EmailLeaseExpiring") + .HasColumnType("INTEGER"); + + b.Property("EmailMaintenanceUpdate") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentReceived") + .HasColumnType("INTEGER"); + + b.Property("EnableDailyDigest") + .HasColumnType("INTEGER"); + + b.Property("EnableEmailNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableInAppNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableSMSNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableWeeklyDigest") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SMSLeaseExpiringUrgent") + .HasColumnType("INTEGER"); + + b.Property("SMSMaintenanceEmergency") + .HasColumnType("INTEGER"); + + b.Property("SMSPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WeeklyDigestDay") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("NotificationPreferences"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("DailyLimit") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentToday") + .HasColumnType("INTEGER"); + + b.Property("EnableSsl") + .HasColumnType("INTEGER"); + + b.Property("FromEmail") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FromName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsEmailEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastEmailSentOn") + .HasColumnType("TEXT"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastErrorOn") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyLimit") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendGridApiKeyEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SmtpPort") + .HasColumnType("INTEGER"); + + b.Property("SmtpServer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationEmailSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountBalance") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("AccountType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CostPerSMS") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSMSEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastSMSSentOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SMSSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("SMSSentToday") + .HasColumnType("INTEGER"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("TwilioAccountSidEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioAuthTokenEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioPhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSMSSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationUsers"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId", "PaymentNumber") + .IsUnique() + .HasDatabaseName("IX_Payment_OrgId_PaymentNumber"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("ContactId") + .HasColumnType("TEXT"); + + b.Property("ContactPerson") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorId") + .HasColumnType("TEXT"); + + b.Property("ContractorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Cost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MaintenanceRequestId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PartsReplaced") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RepairType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("WarrantyApplies") + .HasColumnType("INTEGER"); + + b.Property("WarrantyExpiresOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("LeaseId"); + + b.HasIndex("MaintenanceRequestId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RepairType"); + + b.ToTable("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActiveOrganizationId"); + + b.HasIndex("Email"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserProfiles"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EmailError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EmailSent") + .HasColumnType("INTEGER"); + + b.Property("EmailSentOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRead") + .HasColumnType("INTEGER"); + + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReadOn") + .HasColumnType("TEXT"); + + b.Property("RecipientUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RelatedEntityId") + .HasColumnType("TEXT"); + + b.Property("RelatedEntityType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SMSError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SMSSent") + .HasColumnType("INTEGER"); + + b.Property("SMSSentOn") + .HasColumnType("TEXT"); + + b.Property("SendEmail") + .HasColumnType("INTEGER"); + + b.Property("SendInApp") + .HasColumnType("INTEGER"); + + b.Property("SendSMS") + .HasColumnType("INTEGER"); + + b.Property("SentOn") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsRead"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("SentOn"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("MaintenanceRequests") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Repair", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.MaintenanceRequest", "MaintenanceRequest") + .WithMany("Repairs") + .HasForeignKey("MaintenanceRequestId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Repairs") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("MaintenanceRequest"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + + b.Navigation("MaintenanceRequests"); + + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260216205819_ConsolidateOrganizationIdToBaseModel.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260216205819_ConsolidateOrganizationIdToBaseModel.cs new file mode 100644 index 0000000..a62aca5 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260216205819_ConsolidateOrganizationIdToBaseModel.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Infrastructure.Migrations +{ + /// + /// Documentation-only migration: Moved OrganizationId property from individual entity classes to BaseModel. + /// This is a code refactoring that eliminates duplicate property declarations across 30+ entities. + /// No database schema changes - OrganizationId columns already existed in all tables and remain unchanged. + /// UserProfile shadows BaseModel.OrganizationId with 'new' keyword to maintain nullable semantics. + /// + /// + public partial class ConsolidateOrganizationIdToBaseModel : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 60dcd57..7c0bbdb 100644 --- a/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -68,6 +68,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -143,6 +146,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -229,6 +235,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -290,6 +299,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -361,6 +373,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("ItemOrder") .HasColumnType("INTEGER"); @@ -429,6 +444,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("IsSystemTemplate") .HasColumnType("INTEGER"); @@ -464,6 +482,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), Description = "Standard property showing checklist", IsDeleted = false, + IsSampleData = false, IsSystemTemplate = true, Name = "Property Tour", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") @@ -476,6 +495,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), Description = "Move-in inspection checklist", IsDeleted = false, + IsSampleData = false, IsSystemTemplate = true, Name = "Move-In", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") @@ -488,6 +508,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), Description = "Move-out inspection checklist", IsDeleted = false, + IsSampleData = false, IsSystemTemplate = true, Name = "Move-Out", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") @@ -500,6 +521,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), Description = "Open house event checklist", IsDeleted = false, + IsSampleData = false, IsSystemTemplate = true, Name = "Open House", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") @@ -535,6 +557,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsRequired") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("ItemOrder") .HasColumnType("INTEGER"); @@ -576,6 +601,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 1, ItemText = "Greeted prospect and verified appointment", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -592,6 +618,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 2, ItemText = "Reviewed property exterior and curb appeal", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -608,6 +635,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 3, ItemText = "Showed parking area/garage", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -624,6 +652,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 4, ItemText = "Toured living room/common areas", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -640,6 +669,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 5, ItemText = "Showed all bedrooms", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -656,6 +686,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 6, ItemText = "Showed all bathrooms", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -672,6 +703,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 7, ItemText = "Toured kitchen and demonstrated appliances", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -688,6 +720,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 8, ItemText = "Explained which appliances are included", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -704,6 +737,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 9, ItemText = "Explained HVAC system and thermostat controls", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -720,6 +754,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 10, ItemText = "Reviewed utility responsibilities (tenant vs landlord)", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -736,6 +771,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 11, ItemText = "Showed water heater location", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -752,6 +788,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 12, ItemText = "Showed storage areas (closets, attic, basement)", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -768,6 +805,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 13, ItemText = "Showed laundry facilities", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -784,6 +822,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 14, ItemText = "Showed outdoor space (yard, patio, balcony)", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -800,6 +839,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 15, ItemText = "Discussed monthly rent amount", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -816,6 +856,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 16, ItemText = "Explained security deposit and move-in costs", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -832,6 +873,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 17, ItemText = "Reviewed lease term length and start date", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -848,6 +890,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 18, ItemText = "Explained pet policy", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -864,6 +907,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 19, ItemText = "Explained application process and requirements", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -880,6 +924,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 20, ItemText = "Reviewed screening process (background, credit check)", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -896,6 +941,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 21, ItemText = "Answered all prospect questions", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -912,6 +958,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 22, ItemText = "Prospect Interest Level", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -928,6 +975,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 23, ItemText = "Overall showing feedback and notes", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -944,6 +992,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 1, ItemText = "Document property condition", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -960,6 +1009,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 2, ItemText = "Collect keys and access codes", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -976,6 +1026,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 3, ItemText = "Review lease terms with tenant", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -992,6 +1043,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 1, ItemText = "Inspect property condition", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -1008,6 +1060,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 2, ItemText = "Collect all keys and access devices", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -1024,6 +1077,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 3, ItemText = "Document damages and needed repairs", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -1040,6 +1094,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 1, ItemText = "Set up signage and directional markers", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -1056,6 +1111,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 2, ItemText = "Prepare information packets", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -1072,6 +1128,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), IsDeleted = false, IsRequired = true, + IsSampleData = false, ItemOrder = 3, ItemText = "Set up visitor sign-in sheet", OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), @@ -1170,6 +1227,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -1181,7 +1241,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("PaymentId") @@ -1360,6 +1419,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("KitchenAppliancesGood") .HasColumnType("INTEGER"); @@ -1401,7 +1463,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("OverallCondition") @@ -1480,6 +1541,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -1505,7 +1569,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("PaidOn") @@ -1526,12 +1589,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("DocumentId"); - b.HasIndex("InvoiceNumber") - .IsUnique(); - b.HasIndex("LeaseId"); - b.HasIndex("OrganizationId"); + b.HasIndex("OrganizationId", "InvoiceNumber") + .IsUnique() + .HasDatabaseName("IX_Invoice_OrgId_InvoiceNumber"); b.ToTable("Invoices"); }); @@ -1570,6 +1632,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -1698,6 +1763,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -1717,7 +1785,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("PropertyId") @@ -1803,6 +1870,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -1909,6 +1979,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -1917,7 +1990,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("UserFullName") @@ -1985,6 +2057,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -2117,6 +2192,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsEmailEnabled") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("IsVerified") .HasColumnType("INTEGER"); @@ -2221,6 +2299,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsSMSEnabled") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("IsVerified") .HasColumnType("INTEGER"); @@ -2320,6 +2401,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -2350,7 +2434,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("OrganizationId") .IsRequired() - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("OrganizationSharePercentage") @@ -2470,6 +2553,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -2483,7 +2569,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("PaidOn") @@ -2505,7 +2590,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("InvoiceId"); - b.HasIndex("OrganizationId"); + b.HasIndex("OrganizationId", "PaymentNumber") + .IsUnique() + .HasDatabaseName("IX_Payment_OrgId_PaymentNumber"); b.ToTable("Payments"); }); @@ -2552,6 +2639,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -2570,7 +2660,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("PropertyType") @@ -2659,6 +2748,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -2676,7 +2768,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("Phone") @@ -2785,6 +2876,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("JobTitle") .IsRequired() .HasMaxLength(100) @@ -2812,7 +2906,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("decimal(18,2)"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("PropertyId") @@ -2920,6 +3013,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -3035,6 +3131,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -3050,7 +3149,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("PaymentMethod") @@ -3135,6 +3233,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -3157,7 +3258,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationId") - .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("PaymentMethod") @@ -3240,6 +3340,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -3344,6 +3447,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -3422,6 +3528,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -3490,6 +3599,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -3557,6 +3669,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); @@ -3637,6 +3752,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsRead") .HasColumnType("INTEGER"); + b.Property("IsSampleData") + .HasColumnType("INTEGER"); + b.Property("LastModifiedBy") .HasMaxLength(100) .HasColumnType("TEXT"); diff --git a/1-Aquiis.Infrastructure/Hubs/NotificationHub.cs b/1-Aquiis.Infrastructure/Hubs/NotificationHub.cs new file mode 100644 index 0000000..2e26d84 --- /dev/null +++ b/1-Aquiis.Infrastructure/Hubs/NotificationHub.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; + +namespace Aquiis.Infrastructure.Hubs; + +/// +/// SignalR hub for real-time notification updates across browser tabs and devices. +/// Provides instant synchronization of notification state (read/unread/deleted) for users. +/// Broadcasting is handled by NotificationService using IHubContext, not directly through hub methods. +/// +[AllowAnonymous] // Blazor Server circuits already authenticated - no additional auth needed +public class NotificationHub : Hub +{ + private readonly ILogger _logger; + + public NotificationHub(ILogger logger) + { + _logger = logger; + } + + /// + /// Called when a client connects to the hub. + /// Logs connection for monitoring and debugging. + /// + public override async Task OnConnectedAsync() + { + var userId = Context.User?.Identity?.Name; + _logger.LogInformation($"User {userId} connected to NotificationHub with ConnectionId: {Context.ConnectionId}"); + await base.OnConnectedAsync(); + } + + /// + /// Called when a client disconnects from the hub. + /// Logs disconnection for monitoring and debugging. + /// + public override async Task OnDisconnectedAsync(Exception? exception) + { + var userId = Context.User?.Identity?.Name; + _logger.LogInformation($"User {userId} disconnected from NotificationHub. ConnectionId: {Context.ConnectionId}"); + if (exception != null) + { + _logger.LogError(exception, $"User {userId} disconnected with error"); + } + await base.OnDisconnectedAsync(exception); + } +} diff --git a/2-Aquiis.Application/Services/NotificationService.cs b/2-Aquiis.Application/Services/NotificationService.cs index 5e9c7fd..bd8a6c6 100644 --- a/2-Aquiis.Application/Services/NotificationService.cs +++ b/2-Aquiis.Application/Services/NotificationService.cs @@ -5,12 +5,15 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.SignalR; +using Aquiis.Infrastructure.Hubs; namespace Aquiis.Application.Services; public class NotificationService : BaseService { private readonly IEmailService _emailService; private readonly ISMSService _smsService; + private readonly IHubContext _hubContext; public NotificationService( ApplicationDbContext context, @@ -18,11 +21,13 @@ public NotificationService( IEmailService emailService, ISMSService smsService, IOptions appSettings, + IHubContext hubContext, ILogger logger) : base(context, logger, userContext, appSettings) { _emailService = emailService; _smsService = smsService; + _hubContext = hubContext; } /// @@ -104,6 +109,9 @@ await _smsService.SendSMSAsync( await UpdateAsync(notification); + // Broadcast new notification via SignalR + await BroadcastNewNotificationAsync(notification); + return notification; } @@ -151,6 +159,28 @@ public async Task MarkAsReadAsync(Guid notificationId) notification.ReadOn = DateTime.UtcNow; await UpdateAsync(notification); + + // Broadcast notification read event via SignalR + var unreadCount = await GetUnreadCountAsync(notification.RecipientUserId); + await BroadcastNotificationReadAsync(notificationId, notification.RecipientUserId, unreadCount); + } + + /// + /// Mark notification as unread + /// + public async Task MarkAsUnreadAsync(Guid notificationId) + { + var notification = await GetByIdAsync(notificationId); + if (notification == null) return; + + notification.IsRead = false; + notification.ReadOn = null; + + await UpdateAsync(notification); + + // Broadcast updated unread count via SignalR + var unreadCount = await GetUnreadCountAsync(notification.RecipientUserId); + await BroadcastUnreadCountChangedAsync(notification.RecipientUserId, unreadCount); } /// @@ -158,6 +188,8 @@ public async Task MarkAsReadAsync(Guid notificationId) /// public async Task MarkAllAsReadAsync(List notifications) { + var userId = notifications.FirstOrDefault()?.RecipientUserId; + foreach (var notification in notifications) { if (!notification.IsRead) @@ -167,6 +199,13 @@ public async Task MarkAllAsReadAsync(List notifications) await UpdateAsync(notification); } } + + // Broadcast updated unread count via SignalR + if (userId != null) + { + var unreadCount = await GetUnreadCountAsync(userId); + await BroadcastUnreadCountChangedAsync(userId, unreadCount); + } } /// @@ -309,4 +348,141 @@ public async Task SendEmailDirectAsync(string to, string subject, string body, s { await _emailService.SendEmailAsync(to, subject, body, fromName); } + + /// + /// Delete a notification + /// + public async Task DeleteNotificationAsync(Guid notificationId) + { + var notification = await GetByIdAsync(notificationId); + if (notification == null) return; + + var userId = notification.RecipientUserId; + await DeleteAsync(notificationId); + + // Broadcast notification deleted event via SignalR + var unreadCount = await GetUnreadCountAsync(userId); + await BroadcastNotificationDeletedAsync(notificationId, userId, unreadCount); + } + + /// + /// Get unread count for a specific user + /// + private async Task GetUnreadCountAsync(string userId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Notifications + .CountAsync(n => n.OrganizationId == organizationId + && n.RecipientUserId == userId + && !n.IsRead + && !n.IsDeleted); + } + + #region SignalR Broadcasting + + /// + /// Broadcasts a new notification to the user via SignalR + /// + private async Task BroadcastNewNotificationAsync(Notification notification) + { + try + { + await _hubContext.Clients + .User(notification.RecipientUserId) + .SendAsync("ReceiveNotification", new + { + notification.Id, + notification.Title, + notification.Message, + notification.Type, + notification.Category, + notification.SentOn, + notification.IsRead + }); + + _logger.LogInformation( + "Broadcasted notification {NotificationId} to user {UserId} via SignalR", + notification.Id, + notification.RecipientUserId); + } + catch (Exception ex) + { + // Don't fail notification creation if SignalR fails + _logger.LogWarning(ex, + "Failed to broadcast notification {NotificationId} via SignalR", + notification.Id); + } + } + + /// + /// Broadcasts that a notification was marked as read + /// + private async Task BroadcastNotificationReadAsync(Guid notificationId, string userId, int newUnreadCount) + { + try + { + await _hubContext.Clients + .User(userId) + .SendAsync("NotificationRead", notificationId, newUnreadCount); + + _logger.LogInformation( + "Broadcasted notification read {NotificationId} to user {UserId} via SignalR", + notificationId, + userId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "Failed to broadcast notification read via SignalR"); + } + } + + /// + /// Broadcasts that a notification was deleted + /// + private async Task BroadcastNotificationDeletedAsync(Guid notificationId, string userId, int newUnreadCount) + { + try + { + await _hubContext.Clients + .User(userId) + .SendAsync("NotificationDeleted", notificationId, newUnreadCount); + + _logger.LogInformation( + "Broadcasted notification deleted {NotificationId} to user {UserId} via SignalR", + notificationId, + userId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "Failed to broadcast notification deletion via SignalR"); + } + } + + /// + /// Broadcasts updated unread count (e.g., mark all as read) + /// + private async Task BroadcastUnreadCountChangedAsync(string userId, int newUnreadCount) + { + try + { + await _hubContext.Clients + .User(userId) + .SendAsync("UpdateUnreadCount", newUnreadCount); + + _logger.LogInformation( + "Broadcasted unread count {Count} to user {UserId} via SignalR", + newUnreadCount, + userId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "Failed to broadcast unread count change via SignalR"); + } + } + + #endregion } \ No newline at end of file diff --git a/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs b/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs index 9fbba45..24bfc77 100644 --- a/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs +++ b/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs @@ -16,6 +16,7 @@ public class SampleDataWorkflowService : BaseWorkflowService private readonly ILogger _logger; private readonly InvoiceService _invoiceService; private readonly PaymentService _paymentService; + private readonly NotificationService _notificationService; private readonly Random _random; public SampleDataWorkflowService( @@ -23,11 +24,13 @@ public SampleDataWorkflowService( IUserContextService userContext, InvoiceService invoiceService, PaymentService paymentService, + NotificationService notificationService, ILogger logger) : base(context, userContext) { _logger = logger; _invoiceService = invoiceService; _paymentService = paymentService; + _notificationService = notificationService; _random = new Random(DateTime.Now.Millisecond); // Seed for varied data } @@ -58,6 +61,12 @@ public async Task GenerateSampleDataAsync() var properties = await GeneratePropertiesAsync(orgId, systemUserId); _logger.LogInformation($"Created {properties.Count} properties"); + var calendarEvents = await GenerateCalendarEventsForPropertiesAsync(properties); + _logger.LogInformation($"Created {calendarEvents.Count} calendar events"); + + var notifications = await GenerateNotificationsForRoutineInspections(properties); + _logger.LogInformation($"Created {notifications.Count} notifications"); + var tenants = await GenerateTenantsAsync(orgId, systemUserId); _logger.LogInformation($"Created {tenants.Count} tenants"); @@ -127,6 +136,12 @@ public async Task RemoveSampleDataAsync() var tenantsDeleted = await RemoveTenantsAsync(orgId, systemUserId); _logger.LogInformation($"Deleted {tenantsDeleted} tenants"); + var calendarEventsDeleted = await RemoveCalendarEventsAsync(orgId, systemUserId); + _logger.LogInformation($"Deleted {calendarEventsDeleted} calendar events"); + + var notificationsDeleted = await RemoveNotificationsAsync(orgId, systemUserId); + _logger.LogInformation($"Deleted {notificationsDeleted} notifications"); + var propertiesDeleted = await RemovePropertiesAsync(orgId, systemUserId); _logger.LogInformation($"Deleted {propertiesDeleted} properties"); @@ -137,12 +152,12 @@ await LogTransitionAsync( fromStatus: "Generated", toStatus: "Removed", action: "RemoveSampleData", - reason: $"Deleted {propertiesDeleted} properties, {tenantsDeleted} tenants, {leasesDeleted} leases, {invoicesDeleted} invoices, {paymentsDeleted} payments" + reason: $"Deleted {propertiesDeleted} properties, {tenantsDeleted} tenants, {leasesDeleted} leases, {invoicesDeleted} invoices, {paymentsDeleted} payments, {calendarEventsDeleted} calendar events" ); return WorkflowResult.Ok( $"Successfully removed sample data: {propertiesDeleted} properties, {tenantsDeleted} tenants, " + - $"{leasesDeleted} leases, {invoicesDeleted} invoices, {paymentsDeleted} payments"); + $"{leasesDeleted} leases, {invoicesDeleted} invoices, {paymentsDeleted} payments, {calendarEventsDeleted} calendar events"); } catch (Exception ex) { @@ -193,10 +208,11 @@ private async Task> GeneratePropertiesAsync(Guid organizationId, Description = $"Beautiful {data.Beds} bedroom, {data.Baths} bath {data.Type.ToLower()} in {data.City}. " + $"{data.SqFt} square feet with modern amenities and convenient location.", RoutineInspectionIntervalMonths = 12, - NextRoutineInspectionDueDate = DateTime.Today.AddMonths(6), + NextRoutineInspectionDueDate = createdDate.AddDays(30), CreatedBy = userId, CreatedOn = createdDate, - IsDeleted = false + IsDeleted = false, + IsSampleData = true }; _context.Properties.Add(property); @@ -239,14 +255,15 @@ private async Task> GenerateTenantsAsync(Guid organizationId, strin Email = $"{data.FirstName.ToLower()}.{data.LastName.ToLower()}@example.com", PhoneNumber = $"555-{_random.Next(100, 999)}-{_random.Next(1000, 9999)}", DateOfBirth = data.DOB, - IdentificationNumber = $"DL-{_random.Next(10000000, 99999999)}", + IdentificationNumber = $"ID-{_random.Next(10000000, 99999999)}", IsActive = true, EmergencyContactName = data.EmergencyName, EmergencyContactPhone = data.EmergencyPhone, Notes = $"Emergency contact relationship: {data.Relationship}", CreatedBy = userId, CreatedOn = createdDate, - IsDeleted = false + IsDeleted = false, + IsSampleData = true }; _context.Tenants.Add(tenant); @@ -302,7 +319,8 @@ private async Task> GenerateLeasesAsync( OfferedOn = startDate.AddDays(-20), // Offered 20 days before start CreatedBy = userId, CreatedOn = startDate.AddDays(-25), - IsDeleted = false + IsDeleted = false, + IsSampleData = true }; _context.Leases.Add(lease); @@ -362,7 +380,8 @@ private async Task> GenerateInvoicesAsync( Description = $"Monthly Rent - {invoiceDate:MMMM yyyy}", CreatedBy = userId, CreatedOn = invoiceDate, - IsDeleted = false + IsDeleted = false, + IsSampleData = true }; _context.Invoices.Add(invoice); @@ -442,7 +461,8 @@ private async Task> GeneratePaymentsAsync( Notes = $"Payment for {invoice.Description}", CreatedBy = userId, CreatedOn = paymentDate, - IsDeleted = false + IsDeleted = false, + IsSampleData = true }; _context.Payments.Add(payment); @@ -472,6 +492,85 @@ private async Task> GeneratePaymentsAsync( #endregion + #region Calendar Event Generation + + private async Task> GenerateCalendarEventsForPropertiesAsync(List properties) + { + var calendarEvents = new List(); + + foreach (var property in properties) + { + if (!property.NextRoutineInspectionDueDate.HasValue) + { + continue; + } + + var calendarEvent = new CalendarEvent + { + Id = Guid.NewGuid(), + Title = $"Routine Inspection - {property.Address}", + Description = $"Scheduled routine inspection for property at {property.Address}", + StartOn = property.NextRoutineInspectionDueDate.Value, + EndOn = property.NextRoutineInspectionDueDate.Value.AddHours(1), + DurationMinutes = 60, + Location = property.Address, + SourceEntityType = nameof(Property), + SourceEntityId = property.Id, + PropertyId = property.Id, + OrganizationId = property.OrganizationId, + CreatedBy = property.CreatedBy, + CreatedOn = DateTime.UtcNow, + EventType = "Inspection", + Status = "Scheduled", + IsSampleData = true + }; + + calendarEvents.Add(calendarEvent); + } + _context.CalendarEvents.AddRange(calendarEvents); + await _context.SaveChangesAsync(); + return calendarEvents; + } + + #endregion + + #region Notification Generation + private async Task> GenerateNotificationsForRoutineInspections(List properties) + { + if (properties == null || properties.Count == 0) + return new List(); + + var notifications = new List(); + var users = await _context.OrganizationUsers + .Where(o => o.OrganizationId == properties.First().OrganizationId && !o.IsDeleted && o.IsActive).ToListAsync(); + + foreach(var user in users) + { + foreach(var property in properties) + { + // Use NotificationService to send notifications with SignalR broadcasts + var notification = await _notificationService.SendNotificationAsync( + user.UserId, + "Routine Inspection Scheduled", + $"A routine inspection has been scheduled for the property at {property.Address} on {property.NextRoutineInspectionDueDate!.Value:d}.", + NotificationConstants.Types.Info, + NotificationConstants.Categories.Inspection, + property.Id, + nameof(Property) + ); + + // Mark as sample data + notification.IsSampleData = true; + await _context.SaveChangesAsync(); + + notifications.Add(notification); + } + } + + return notifications; + } + #endregion + #region Helper Methods /// @@ -506,7 +605,7 @@ private string GetRandomPaymentMethod() private async Task RemovePropertiesAsync(Guid organizationId, string systemUserId) { var properties = await _context.Properties - .Where(p => p.OrganizationId == organizationId && p.CreatedBy == systemUserId) + .Where(p => p.OrganizationId == organizationId && p.IsSampleData) .ToListAsync(); _context.Properties.RemoveRange(properties); @@ -518,7 +617,7 @@ private async Task RemovePropertiesAsync(Guid organizationId, string system private async Task RemoveTenantsAsync(Guid organizationId, string systemUserId) { var tenants = await _context.Tenants - .Where(t => t.OrganizationId == organizationId && t.CreatedBy == systemUserId) + .Where(t => t.OrganizationId == organizationId && t.IsSampleData) .ToListAsync(); _context.Tenants.RemoveRange(tenants); @@ -530,7 +629,7 @@ private async Task RemoveTenantsAsync(Guid organizationId, string systemUse private async Task RemoveLeasesAsync(Guid organizationId, string systemUserId) { var leases = await _context.Leases - .Where(l => l.OrganizationId == organizationId && l.CreatedBy == systemUserId) + .Where(l => l.OrganizationId == organizationId && l.IsSampleData) .ToListAsync(); _context.Leases.RemoveRange(leases); @@ -542,7 +641,7 @@ private async Task RemoveLeasesAsync(Guid organizationId, string systemUser private async Task RemoveInvoicesAsync(Guid organizationId, string systemUserId) { var invoices = await _context.Invoices - .Where(i => i.OrganizationId == organizationId && i.CreatedBy == systemUserId) + .Where(i => i.OrganizationId == organizationId && i.IsSampleData) .ToListAsync(); _context.Invoices.RemoveRange(invoices); @@ -554,7 +653,7 @@ private async Task RemoveInvoicesAsync(Guid organizationId, string systemUs private async Task RemovePaymentsAsync(Guid organizationId, string systemUserId) { var payments = await _context.Payments - .Where(p => p.OrganizationId == organizationId && p.CreatedBy == systemUserId) + .Where(p => p.OrganizationId == organizationId && p.IsSampleData) .ToListAsync(); _context.Payments.RemoveRange(payments); @@ -563,6 +662,125 @@ private async Task RemovePaymentsAsync(Guid organizationId, string systemUs return payments.Count; } + private async Task RemoveCalendarEventsAsync(Guid organizationId, string systemUserId) + { + var calendarEvents = await _context.CalendarEvents + .Where(ce => ce.OrganizationId == organizationId && ce.IsSampleData) + .ToListAsync(); + + _context.CalendarEvents.RemoveRange(calendarEvents); + await _context.SaveChangesAsync(); + + return calendarEvents.Count; + } + + private async Task RemoveNotificationsAsync(Guid organizationId, string systemUserId) + { + var notifications = await _context.Notifications + .Where(n => n.OrganizationId == organizationId && n.IsSampleData) + .ToListAsync(); + + _context.Notifications.RemoveRange(notifications); + await _context.SaveChangesAsync(); + + return notifications.Count; + } + + // + // Potential future method to remove all sample data in one go (if needed) - currently we remove by entity type to ensure proper order and avoid FK issues + private async Task RemoveAllSampleDataAsync(Guid orgId) + { + var allSampleEntities = new List(); + + foreach (var dbSet in _context.GetType().GetProperties().Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))) + { + var entity = dbSet.GetValue(_context); + if (entity is IQueryable baseModelQueryable) + { + var sampleData = baseModelQueryable + .Where(e => e.OrganizationId == orgId && e.IsSampleData) + .ToList(); + allSampleEntities.AddRange(sampleData); + } + } + + _context.RemoveRange(allSampleEntities); // EF Core figures out order + await _context.SaveChangesAsync(); + } + #endregion + + #region Sample Data Detection + + /// + /// Checks if sample data exists for the active organization. + /// Used to conditionally show Add/Remove Sample Data buttons. + /// + public async Task HasSampleDataAsync() + { + var orgId = await GetActiveOrganizationIdAsync(); + + // Check any entity type - if any sample data exists, return true + var hasSampleData = await _context.Properties + .AnyAsync(p => p.OrganizationId == orgId && p.IsSampleData && !p.IsDeleted); + + if (!hasSampleData) + { + hasSampleData = await _context.Tenants + .AnyAsync(t => t.OrganizationId == orgId && t.IsSampleData && !t.IsDeleted); + } + + if (!hasSampleData) + { + hasSampleData = await _context.Leases + .AnyAsync(l => l.OrganizationId == orgId && l.IsSampleData && !l.IsDeleted); + } + + if (!hasSampleData) + { + hasSampleData = await _context.CalendarEvents + .AnyAsync(ce => ce.OrganizationId == orgId && ce.IsSampleData && !ce.IsDeleted); + } + + return hasSampleData; + } + + /// + /// Gets count of sample data records by entity type. + /// Used for UI display (e.g., "3 sample properties, 2 sample tenants"). + /// + public async Task GetSampleDataSummaryAsync() + { + var orgId = await GetActiveOrganizationIdAsync(); + + return new SampleDataSummary + { + PropertyCount = await _context.Properties.CountAsync(p => p.OrganizationId == orgId && p.IsSampleData && !p.IsDeleted), + TenantCount = await _context.Tenants.CountAsync(t => t.OrganizationId == orgId && t.IsSampleData && !t.IsDeleted), + LeaseCount = await _context.Leases.CountAsync(l => l.OrganizationId == orgId && l.IsSampleData && !l.IsDeleted), + InvoiceCount = await _context.Invoices.CountAsync(i => i.OrganizationId == orgId && i.IsSampleData && !i.IsDeleted), + PaymentCount = await _context.Payments.CountAsync(p => p.OrganizationId == orgId && p.IsSampleData && !p.IsDeleted), + CalendarEventCount = await _context.CalendarEvents.CountAsync(ce => ce.OrganizationId == orgId && ce.IsSampleData && !ce.IsDeleted) + }; + } + + #endregion + } + + /// + /// Summary of sample data counts by entity type. + /// + public class SampleDataSummary + { + public int PropertyCount { get; set; } + public int TenantCount { get; set; } + public int LeaseCount { get; set; } + public int InvoiceCount { get; set; } + public int PaymentCount { get; set; } + public int CalendarEventCount { get; set; } + public int NotificationCount { get; set; } + + public int TotalCount => PropertyCount + TenantCount + LeaseCount + InvoiceCount + PaymentCount + CalendarEventCount + NotificationCount; + public bool HasData => TotalCount > 0; } } diff --git a/3-Aquiis.UI.Shared/Aquiis.UI.Shared.csproj b/3-Aquiis.UI.Shared/Aquiis.UI.Shared.csproj index d2a1ecc..634eb59 100644 --- a/3-Aquiis.UI.Shared/Aquiis.UI.Shared.csproj +++ b/3-Aquiis.UI.Shared/Aquiis.UI.Shared.csproj @@ -13,6 +13,7 @@ + diff --git a/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor index 66a6835..39f0c35 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor @@ -1,5 +1,6 @@ @using Aquiis.Core.Entities @using Aquiis.Application.Services +@using Aquiis.Application.Services.Workflows @using Aquiis.Core.Constants @namespace Aquiis.UI.Shared.Components.Entities.Organizations @@ -14,18 +15,37 @@ @if (IsOwner) { - - + @if (HasSampleData) + { + + } + else + { + + } } @if(OrganizationViewModel?.Organization != null) {
+ @if (HasSampleData && SampleDataSummary?.HasData == true) + { +
+ + Sample Data Present: + @SampleDataSummary.PropertyCount properties, + @SampleDataSummary.TenantCount tenants, + @SampleDataSummary.LeaseCount leases, + @SampleDataSummary.InvoiceCount invoices, + @SampleDataSummary.PaymentCount payments, + @SampleDataSummary.CalendarEventCount calendar events +
+ }
Organization Name:
@OrganizationViewModel?.Organization?.Name
@@ -95,4 +115,10 @@ [Parameter] public bool IsRemovingSample { get; set; } + [Parameter] + public bool HasSampleData { get; set; } + + [Parameter] + public SampleDataSummary? SampleDataSummary { get; set; } + } \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyCard.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyCard.razor index 6c5eeea..3952ea7 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyCard.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyCard.razor @@ -6,9 +6,17 @@
@Address
- - @Status - +
+ @if (@IsSampleData) + { + + Sample Data +    + } + + @Status + +

@City, @State @ZipCode

@@ -132,6 +140,9 @@ /// [Parameter, EditorRequired] public string Status { get; set; } = string.Empty; + + [Parameter] + public bool IsSampleData { get; set; } = false; /// /// Whether to show action buttons diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor index 26907dc..9034f45 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor @@ -5,9 +5,18 @@
Property Information
- - @Property.Status - +
+ @if (Property.IsSampleData) + { + + Sample Data +    + } + + @Property.Status + + +
@if (OnEdit.HasDelegate) {
+ @if (property.IsSampleData) + { + + Sample Data +    + } @FormatPropertyStatus(property.Status)
@property.MonthlyRent.ToString("C") diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor index d36a091..064e769 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor @@ -55,6 +55,7 @@ State="@property.State" ZipCode="@property.ZipCode" Description="@property.Description" + IsSampleData="@property.IsSampleData" Bedrooms="@property.Bedrooms" Bathrooms="@property.Bathrooms" SquareFeet="@property.SquareFeet" diff --git a/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor b/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor index 0fcca59..d9ff52c 100644 --- a/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor +++ b/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor @@ -1,7 +1,9 @@ @using Aquiis.Application.Services +@using Microsoft.AspNetCore.SignalR.Client @inject NotificationService NotificationService @inject NavigationManager NavigationManager @namespace Aquiis.UI.Shared.Components.Notifications +@implements IAsyncDisposable @if (isLoading) { @@ -113,6 +115,7 @@ else [Parameter] public Func? GetEntityRoute { get; set; } private Notification? selectedNotification; + private HubConnection? _hubConnection; private bool showNotificationModal = false; @@ -125,6 +128,77 @@ else protected override async Task OnInitializedAsync() { await LoadNotificationsAsync(); + await InitializeSignalRConnectionAsync(); + } + + private async Task InitializeSignalRConnectionAsync() + { + try + { + _hubConnection = new HubConnectionBuilder() + .WithUrl(NavigationManager.ToAbsoluteUri("/hubs/notifications")) + .WithAutomaticReconnect() // Auto-reconnect on disconnect + .Build(); + + // Listen for new notifications + _hubConnection.On("ReceiveNotification", async (notification) => + { + await HandleNewNotificationAsync(notification); + }); + + // Listen for read notifications + _hubConnection.On("NotificationRead", (notificationId, newCount) => + { + HandleNotificationRead(notificationId, newCount); + }); + + // Listen for deleted notifications + _hubConnection.On("NotificationDeleted", (notificationId, newCount) => + { + HandleNotificationDeleted(notificationId, newCount); + }); + + // Listen for unread count updates + _hubConnection.On("UpdateUnreadCount", (newCount) => + { + notificationCount = newCount; + _ = InvokeAsync(StateHasChanged); + }); + + await _hubConnection.StartAsync(); + } + catch (Exception ex) + { + // Log error but don't fail component initialization + Console.WriteLine($"SignalR connection failed: {ex.Message}"); + } + } + + private async Task HandleNewNotificationAsync(object notificationData) + { + // Reload notifications to include the new one + await LoadNotificationsAsync(); + await InvokeAsync(StateHasChanged); + } + + private void HandleNotificationRead(Guid notificationId, int newCount) + { + var notification = notifications.FirstOrDefault(n => n.Id == notificationId); + if (notification != null) + { + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + } + + notificationCount = newCount; + _ = InvokeAsync(StateHasChanged); + } + + private void HandleNotificationDeleted(Guid notificationId, int newCount) + { + notifications.RemoveAll(n => n.Id == notificationId); + notificationCount = newCount; + _ = InvokeAsync(StateHasChanged); } private async Task LoadNotificationsAsync() @@ -132,7 +206,7 @@ else isLoading = true; notifications = await NotificationService.GetUnreadNotificationsAsync(); notifications = notifications.OrderByDescending(n => n.CreatedOn).Take(5).ToList(); - notificationCount = notifications.Count; + notificationCount = notifications.Count(n => !n.IsRead); isLoading = false; } @@ -140,11 +214,24 @@ else private async Task ShowNotification(Notification notification) { selectedNotification = notification; - notification.IsRead = true; - notification.ReadOn = DateTime.UtcNow; - await NotificationService.MarkAsReadAsync(notification.Id); - notificationCount = notifications.Count(n => !n.IsRead); - showNotificationModal = true; + + try + { + // Mark as read in database first + await NotificationService.MarkAsReadAsync(notification.Id); + + // Only update local state after successful database update + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + notificationCount = notifications.Count(n => !n.IsRead); + + showNotificationModal = true; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to mark notification as read: {ex.Message}"); + // Don't update local state if database update failed + } } private void CloseModal() @@ -172,15 +259,27 @@ else private async Task MarkAllAsRead() { - foreach (var notification in notifications) + try { - notification.IsRead = true; - notification.ReadOn = DateTime.UtcNow; + // Mark all as read in database first + await NotificationService.MarkAllAsReadAsync(notifications); + + // Only update local state after successful database update + foreach (var notification in notifications) + { + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + } + notificationCount = 0; + + ToggleDropdown(); + StateHasChanged(); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to mark all notifications as read: {ex.Message}"); + // Don't update local state if database update failed } - notificationCount = 0; - ToggleDropdown(); - StateHasChanged(); - await NotificationService.MarkAllAsReadAsync(notifications); } private void GoToNotificationCenter() @@ -207,6 +306,14 @@ else "Success" => "success", _ => "secondary" }; + + public async ValueTask DisposeAsync() + { + if (_hubConnection is not null) + { + await _hubConnection.DisposeAsync(); + } + } } + @code { private List allEvents = new(); private List filteredEvents = new(); @@ -460,17 +557,59 @@ private int totalPages => (int)Math.Ceiling(filteredEvents.Count / (double)pageSize); private bool firstRenderCompleted = false; + // Date range filtering + private string dateRangeMode = "30"; // 7, 30, 90, overdue, custom + private DateTime customStartDate = DateTime.Today; + private DateTime customEndDate = DateTime.Today.AddDays(30); + private int overdueCount = 0; + + // Sorting + private string sortBy = "date-asc"; // date-asc, date-desc, type, status + protected override async Task OnInitializedAsync() { - await LoadEvents(); - // Initialize with all event types selected foreach (var eventType in CalendarEventTypes.GetAllTypes()) { selectedEventTypes.Add(eventType); } - ApplyFilters(); + // Check for overdue events before loading - this determines initial view mode + await CheckForOverdueEvents(); + + // If overdue tasks exist, start in overdue mode; otherwise use 30-day view + if (overdueCount > 0) + { + dateRangeMode = "overdue"; + } + + // Now load events with the correct date range mode + await LoadEvents(); + ApplyFiltersAndSort(); + } + + private async Task CheckForOverdueEvents() + { + try + { + // Query for overdue events (past events that aren't completed/cancelled) + var today = DateTime.Today; + var startDate = today.AddDays(-365); // Look back 1 year + var endDate = today.AddSeconds(-1); // Up to end of yesterday + + var overdueEvents = await CalendarEventService.GetEventsAsync(startDate, endDate); + + overdueCount = overdueEvents.Count(e => + e.StartOn.Date < today && + !string.IsNullOrEmpty(e.Status) && + e.Status != "Completed" && + e.Status != "Cancelled"); + } + catch + { + // Silently fail - default to 30-day view if check fails + overdueCount = 0; + } } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -502,15 +641,57 @@ { loading = true; - - // Get events for the next 30 days - var startDate = DateTime.Today; - var endDate = DateTime.Today.AddDays(30); + // Calculate date range based on selected mode + DateTime startDate; + DateTime endDate; + + switch (dateRangeMode) + { + case "7": + startDate = DateTime.Today; + endDate = DateTime.Today.AddDays(7); + break; + case "30": + startDate = DateTime.Today; + endDate = DateTime.Today.AddDays(30); + break; + case "90": + startDate = DateTime.Today; + endDate = DateTime.Today.AddDays(90); + break; + case "overdue": + startDate = DateTime.Today.AddDays(-365); // Look back 1 year for overdue + endDate = DateTime.Today.AddSeconds(-1); // Up to end of yesterday + break; + case "custom": + startDate = customStartDate; + endDate = customEndDate; + // Validate custom range + if (endDate < startDate) + { + ToastService.ShowWarning("End date must be after start date. Swapping dates."); + (startDate, endDate) = (endDate, startDate); + customStartDate = startDate; + customEndDate = endDate; + } + break; + default: + startDate = DateTime.Today; + endDate = DateTime.Today.AddDays(30); + break; + } allEvents = await CalendarEventService.GetEventsAsync(startDate, endDate); - allEvents = allEvents.OrderBy(e => e.StartOn).ToList(); - ApplyFilters(); + // Calculate overdue count (events in the past that aren't completed/cancelled) + var today = DateTime.Today; + overdueCount = allEvents.Count(e => + e.StartOn.Date < today && + !string.IsNullOrEmpty(e.Status) && + e.Status != "Completed" && + e.Status != "Cancelled"); + + ApplyFiltersAndSort(); } catch (Exception ex) { @@ -534,18 +715,65 @@ } currentPage = 1; // Reset to first page when filtering - ApplyFilters(); + ApplyFiltersAndSort(); } - private void ApplyFilters() + private void ApplyFiltersAndSort() { + // Apply event type filters filteredEvents = allEvents .Where(e => selectedEventTypes.Contains(e.EventType)) .ToList(); + // Apply sorting + filteredEvents = sortBy switch + { + "date-asc" => filteredEvents.OrderBy(e => e.StartOn).ToList(), + "date-desc" => filteredEvents.OrderByDescending(e => e.StartOn).ToList(), + "type" => filteredEvents.OrderBy(e => e.EventType).ThenBy(e => e.StartOn).ToList(), + "status" => filteredEvents.OrderBy(e => e.Status ?? "zzz").ThenBy(e => e.StartOn).ToList(), + _ => filteredEvents.OrderBy(e => e.StartOn).ToList() + }; + UpdatePagedEvents(); } + private void SetDateRange(string mode) + { + dateRangeMode = mode; + currentPage = 1; // Reset to first page + _ = LoadEvents(); // Fire and forget async + } + + private void SetSort(string sorting) + { + sortBy = sorting; + currentPage = 1; // Reset to first page + ApplyFiltersAndSort(); + } + + private void ResetDateRange() + { + dateRangeMode = "30"; + customStartDate = DateTime.Today; + customEndDate = DateTime.Today.AddDays(30); + currentPage = 1; + _ = LoadEvents(); + } + + private string GetDateRangeDescription() + { + return dateRangeMode switch + { + "7" => "Events for the next 7 days", + "30" => "Events for the next 30 days", + "90" => "Events for the next 90 days", + "overdue" => $"Overdue events ({overdueCount} found)", + "custom" => $"Events from {customStartDate:MMM dd, yyyy} to {customEndDate:MMM dd, yyyy}", + _ => "All scheduled events" + }; + } + private void UpdatePagedEvents() { pagedEvents = filteredEvents diff --git a/4-Aquiis.SimpleStart/Program.cs b/4-Aquiis.SimpleStart/Program.cs index a3ae940..9dc1eb8 100644 --- a/4-Aquiis.SimpleStart/Program.cs +++ b/4-Aquiis.SimpleStart/Program.cs @@ -44,6 +44,9 @@ builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); +// Add SignalR for real-time notification updates +builder.Services.AddSignalR(); + // Add antiforgery services with options for Blazor builder.Services.AddAntiforgery(options => { @@ -618,6 +621,9 @@ // Add additional endpoints required by the Identity /Account Razor components. app.MapAdditionalIdentityEndpoints(); +// Map SignalR hub for real-time notifications +app.MapHub("/hubs/notifications"); + // Add session refresh endpoint for session timeout feature app.MapPost("/api/session/refresh", async (HttpContext context) => { diff --git a/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor b/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor index c428e3e..9402299 100644 --- a/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor +++ b/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor @@ -29,22 +29,109 @@

Calendar - List View

-

All scheduled events for the next 30 days

+

@GetDateRangeDescription()

+ @if (overdueCount > 0) + { + + @overdueCount overdue + + }
+ +
+
+
+
+
Date Range
+
+ + + + + +
+ + @if (dateRangeMode == "custom") + { +
+
+ + +
+
+ + +
+
+ +
+
+ } +
+
+
+
+ @if (showFilters) {
+ + +
+
+
Sort By
+
+ + + + +
+
+
+ +
+ +
Event Types
@foreach (var eventType in CalendarEventTypes.GetAllTypes()) @@ -440,6 +527,16 @@
} + + @code { private List allEvents = new(); private List filteredEvents = new(); @@ -460,17 +557,59 @@ private int totalPages => (int)Math.Ceiling(filteredEvents.Count / (double)pageSize); private bool firstRenderCompleted = false; + // Date range filtering + private string dateRangeMode = "30"; // 7, 30, 90, overdue, custom + private DateTime customStartDate = DateTime.Today; + private DateTime customEndDate = DateTime.Today.AddDays(30); + private int overdueCount = 0; + + // Sorting + private string sortBy = "date-asc"; // date-asc, date-desc, type, status + protected override async Task OnInitializedAsync() { - await LoadEvents(); - // Initialize with all event types selected foreach (var eventType in CalendarEventTypes.GetAllTypes()) { selectedEventTypes.Add(eventType); } - ApplyFilters(); + // Check for overdue events before loading - this determines initial view mode + await CheckForOverdueEvents(); + + // If overdue tasks exist, start in overdue mode; otherwise use 30-day view + if (overdueCount > 0) + { + dateRangeMode = "overdue"; + } + + // Now load events with the correct date range mode + await LoadEvents(); + ApplyFiltersAndSort(); + } + + private async Task CheckForOverdueEvents() + { + try + { + // Query for overdue events (past events that aren't completed/cancelled) + var today = DateTime.Today; + var startDate = today.AddDays(-365); // Look back 1 year + var endDate = today.AddSeconds(-1); // Up to end of yesterday + + var overdueEvents = await CalendarEventService.GetEventsAsync(startDate, endDate); + + overdueCount = overdueEvents.Count(e => + e.StartOn.Date < today && + !string.IsNullOrEmpty(e.Status) && + e.Status != "Completed" && + e.Status != "Cancelled"); + } + catch + { + // Silently fail - default to 30-day view if check fails + overdueCount = 0; + } } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -502,15 +641,57 @@ { loading = true; - - // Get events for the next 30 days - var startDate = DateTime.Today; - var endDate = DateTime.Today.AddDays(30); + // Calculate date range based on selected mode + DateTime startDate; + DateTime endDate; + + switch (dateRangeMode) + { + case "7": + startDate = DateTime.Today; + endDate = DateTime.Today.AddDays(7); + break; + case "30": + startDate = DateTime.Today; + endDate = DateTime.Today.AddDays(30); + break; + case "90": + startDate = DateTime.Today; + endDate = DateTime.Today.AddDays(90); + break; + case "overdue": + startDate = DateTime.Today.AddDays(-365); // Look back 1 year for overdue + endDate = DateTime.Today.AddSeconds(-1); // Up to end of yesterday + break; + case "custom": + startDate = customStartDate; + endDate = customEndDate; + // Validate custom range + if (endDate < startDate) + { + ToastService.ShowWarning("End date must be after start date. Swapping dates."); + (startDate, endDate) = (endDate, startDate); + customStartDate = startDate; + customEndDate = endDate; + } + break; + default: + startDate = DateTime.Today; + endDate = DateTime.Today.AddDays(30); + break; + } allEvents = await CalendarEventService.GetEventsAsync(startDate, endDate); - allEvents = allEvents.OrderBy(e => e.StartOn).ToList(); - ApplyFilters(); + // Calculate overdue count (events in the past that aren't completed/cancelled) + var today = DateTime.Today; + overdueCount = allEvents.Count(e => + e.StartOn.Date < today && + !string.IsNullOrEmpty(e.Status) && + e.Status != "Completed" && + e.Status != "Cancelled"); + + ApplyFiltersAndSort(); } catch (Exception ex) { @@ -534,18 +715,65 @@ } currentPage = 1; // Reset to first page when filtering - ApplyFilters(); + ApplyFiltersAndSort(); } - private void ApplyFilters() + private void ApplyFiltersAndSort() { + // Apply event type filters filteredEvents = allEvents .Where(e => selectedEventTypes.Contains(e.EventType)) .ToList(); + // Apply sorting + filteredEvents = sortBy switch + { + "date-asc" => filteredEvents.OrderBy(e => e.StartOn).ToList(), + "date-desc" => filteredEvents.OrderByDescending(e => e.StartOn).ToList(), + "type" => filteredEvents.OrderBy(e => e.EventType).ThenBy(e => e.StartOn).ToList(), + "status" => filteredEvents.OrderBy(e => e.Status ?? "zzz").ThenBy(e => e.StartOn).ToList(), + _ => filteredEvents.OrderBy(e => e.StartOn).ToList() + }; + UpdatePagedEvents(); } + private void SetDateRange(string mode) + { + dateRangeMode = mode; + currentPage = 1; // Reset to first page + _ = LoadEvents(); // Fire and forget async + } + + private void SetSort(string sorting) + { + sortBy = sorting; + currentPage = 1; // Reset to first page + ApplyFiltersAndSort(); + } + + private void ResetDateRange() + { + dateRangeMode = "30"; + customStartDate = DateTime.Today; + customEndDate = DateTime.Today.AddDays(30); + currentPage = 1; + _ = LoadEvents(); + } + + private string GetDateRangeDescription() + { + return dateRangeMode switch + { + "7" => "Events for the next 7 days", + "30" => "Events for the next 30 days", + "90" => "Events for the next 90 days", + "overdue" => $"Overdue events ({overdueCount} found)", + "custom" => $"Events from {customStartDate:MMM dd, yyyy} to {customEndDate:MMM dd, yyyy}", + _ => "All scheduled events" + }; + } + private void UpdatePagedEvents() { pagedEvents = filteredEvents diff --git a/5-Aquiis.Professional/Program.cs b/5-Aquiis.Professional/Program.cs index a3ff18d..98e090f 100644 --- a/5-Aquiis.Professional/Program.cs +++ b/5-Aquiis.Professional/Program.cs @@ -35,6 +35,9 @@ builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); +// Add SignalR for real-time notification updates +builder.Services.AddSignalR(); + // Add antiforgery services with options for Blazor builder.Services.AddAntiforgery(options => { @@ -582,6 +585,9 @@ // Add additional endpoints required by the Identity /Account Razor components. app.MapAdditionalIdentityEndpoints(); +// Map SignalR hub for real-time notifications +app.MapHub("/hubs/notifications"); + // Add session refresh endpoint for session timeout feature app.MapPost("/api/session/refresh", async (HttpContext context) => { diff --git a/6-Tests/Aquiis.Application.Tests/Services/LeaseWorkflowService.Tests.cs b/6-Tests/Aquiis.Application.Tests/Services/LeaseWorkflowService.Tests.cs index 64427ac..97a3ded 100644 --- a/6-Tests/Aquiis.Application.Tests/Services/LeaseWorkflowService.Tests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/LeaseWorkflowService.Tests.cs @@ -1,6 +1,8 @@ using Aquiis.Core.Entities; using Aquiis.Core.Constants; using Aquiis.Core.Interfaces.Services; +using Aquiis.Infrastructure.Hubs; +using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Moq; @@ -75,6 +77,7 @@ private static async Task CreateTestContextAsync() mockEmailService.Object, mockSmsService.Object, Options.Create(new ApplicationSettings { SoftDeleteEnabled = true }), + Mock.Of>(), Mock.Of>() ); diff --git a/6-Tests/Aquiis.Application.Tests/Services/NotificationServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/NotificationServiceTests.cs index 65cd6ea..47e3ddc 100644 --- a/6-Tests/Aquiis.Application.Tests/Services/NotificationServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/NotificationServiceTests.cs @@ -4,6 +4,8 @@ using Aquiis.Core.Interfaces.Services; using Aquiis.SimpleStart.Entities; using Aquiis.Infrastructure.Data; +using Aquiis.Infrastructure.Hubs; +using Microsoft.AspNetCore.SignalR; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; @@ -94,6 +96,7 @@ public NotificationServiceTests() _mockEmailService.Object, _mockSMSService.Object, settings, + Mock.Of>(), new NullLogger()); } diff --git a/6-Tests/Aquiis.Application.Tests/Services/PaymentServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/PaymentServiceTests.cs index c9939dc..3273c13 100644 --- a/6-Tests/Aquiis.Application.Tests/Services/PaymentServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/PaymentServiceTests.cs @@ -3,6 +3,8 @@ using Aquiis.Core.Entities; using Aquiis.Core.Interfaces.Services; using Aquiis.Infrastructure.Data; +using Aquiis.Infrastructure.Hubs; +using Microsoft.AspNetCore.SignalR; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -163,6 +165,7 @@ public PaymentServiceTests() mockEmailService.Object, mockSmsService.Object, Options.Create(new ApplicationSettings { SoftDeleteEnabled = true }), + Mock.Of>(), Mock.Of>() ); diff --git a/6-Tests/Aquiis.Application.Tests/Services/PropertyServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/PropertyServiceTests.cs index 4b8701f..2d9a094 100644 --- a/6-Tests/Aquiis.Application.Tests/Services/PropertyServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/PropertyServiceTests.cs @@ -5,6 +5,8 @@ using Aquiis.Core.Interfaces.Services; using Aquiis.SimpleStart.Entities; using Aquiis.Infrastructure.Data; +using Aquiis.Infrastructure.Hubs; +using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -99,6 +101,7 @@ public PropertyServiceTests() mockEmailService.Object, mockSMSService.Object, mockSettings, + Mock.Of>(), mockNotificationLogger.Object ); diff --git a/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.EdgeCaseTests.cs b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.EdgeCaseTests.cs index 2721fe8..cbde7cd 100644 --- a/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.EdgeCaseTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.EdgeCaseTests.cs @@ -3,6 +3,8 @@ using Aquiis.Core.Constants; using Aquiis.Core.Interfaces.Services; using Aquiis.Infrastructure.Data; +using Aquiis.Infrastructure.Hubs; +using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Moq; using Microsoft.Extensions.Logging; @@ -80,6 +82,7 @@ private static async Task CreateTestContextAsync() mockEmailService.Object, mockSmsService.Object, Options.Create(new ApplicationSettings { SoftDeleteEnabled = true }), + Mock.Of>(), Mock.Of>() ); diff --git a/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.LeaseLifecycleTests.cs b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.LeaseLifecycleTests.cs index b5f2280..30ab24b 100644 --- a/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.LeaseLifecycleTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.LeaseLifecycleTests.cs @@ -1,6 +1,8 @@ using Aquiis.Core.Entities; using Aquiis.Core.Constants; using Aquiis.Core.Interfaces.Services; +using Aquiis.Infrastructure.Hubs; +using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Moq; using Aquiis.Infrastructure.Data; @@ -67,6 +69,7 @@ public async Task GenerateAndAcceptLeaseOffer_CreatesLeaseAndTenant_UpdatesPrope mockEmailService.Object, mockSmsService.Object, Options.Create(new ApplicationSettings { SoftDeleteEnabled = true }), + Mock.Of>(), Mock.Of>() ); diff --git a/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowServiceTests.cs index d10a6e1..8ef1a2f 100644 --- a/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowServiceTests.cs @@ -1,6 +1,8 @@ using Aquiis.Core.Entities; using Aquiis.Core.Constants; using Aquiis.Core.Interfaces.Services; +using Aquiis.Infrastructure.Hubs; +using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Moq; using Aquiis.Infrastructure.Data; @@ -71,6 +73,7 @@ public async Task GetApplicationWorkflowStateAsync_ReturnsExpectedState() mockEmailService.Object, mockSmsService.Object, Options.Create(new ApplicationSettings { SoftDeleteEnabled = true }), + Mock.Of>(), Mock.Of>() ); From 0e03edf66b753e9448cc7f401b2794897496ae9b Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Wed, 18 Feb 2026 16:39:43 -0600 Subject: [PATCH 09/10] security-enhancements --- 2-Aquiis.Application/Services/BaseService.cs | 161 ++++ .../Services/CalendarEventService.cs | 25 +- .../Services/DatabaseUnlockService.cs | 37 + .../Services/InspectionService.cs | 37 +- .../Workflows/SampleDataWorkflowService.cs | 544 +++++++++++--- .../Entities/Leases/LeaseRenewalList.razor | 3 +- .../Notifications/NotificationBell.razor | 10 + .../Features/DatabaseUnlock/Index.razor | 149 ++++ 5-Aquiis.Professional/Program.cs | 15 + .../Services/SampleDataPropagationTests.cs | 688 ++++++++++++++++++ 10 files changed, 1557 insertions(+), 112 deletions(-) create mode 100644 6-Tests/Aquiis.Application.Tests/Services/SampleDataPropagationTests.cs diff --git a/2-Aquiis.Application/Services/BaseService.cs b/2-Aquiis.Application/Services/BaseService.cs index 2cf1652..1057989 100644 --- a/2-Aquiis.Application/Services/BaseService.cs +++ b/2-Aquiis.Application/Services/BaseService.cs @@ -375,10 +375,171 @@ private void SetOrganizationId(TEntity entity, Guid organizationId) /// protected virtual async Task SetCreateDefaultsAsync(TEntity entity) { + // Automatically propagate IsSampleData flag from parent entities + await InheritSampleDataFlagFromParentsAsync(entity); + await Task.CompletedTask; return entity; } + /// + /// Checks parent entities for IsSampleData flag and propagates to child entity. + /// If any parent entity has IsSampleData = true, the child entity is marked as sample data. + /// This ensures sample data "taints" all related records for proper cleanup. + /// + /// The entity being created + /// Task + protected virtual async Task InheritSampleDataFlagFromParentsAsync(TEntity entity) + { + try + { + // If already marked as sample data, no need to check parents + if (entity.IsSampleData) + { + return; + } + + // Check for common parent relationship properties + var entityType = typeof(TEntity); + var properties = entityType.GetProperties(); + + // Common parent ID properties to check + var parentIdProperties = new[] + { + "PropertyId", + "LeaseId", + "InvoiceId", + "TenantId", + "ProspectiveTenantId", + "RentalApplicationId", + "RepairId", + "InspectionId", + "MaintenanceRequestId", + "OrganizationId" // Check organization itself for multi-tenant sample orgs + }; + + foreach (var parentPropName in parentIdProperties) + { + var parentIdProperty = properties.FirstOrDefault(p => + p.Name == parentPropName && + (p.PropertyType == typeof(Guid) || p.PropertyType == typeof(Guid?))); + + if (parentIdProperty == null) continue; + + var parentId = parentIdProperty.GetValue(entity); + if (parentId == null || (parentId is Guid guidValue && guidValue == Guid.Empty)) continue; + + // Determine parent entity type and check IsSampleData flag + bool parentIsSampleData = await CheckParentEntityIsSampleDataAsync(parentPropName, (Guid)parentId); + + if (parentIsSampleData) + { + entity.IsSampleData = true; + _logger.LogInformation( + $"{typeof(TEntity).Name} marked as sample data - inherited from {parentPropName} ({parentId})"); + return; // Once marked as sample data, no need to check other parents + } + } + } + catch (Exception ex) + { + // Log but don't fail entity creation if sample data check fails + _logger.LogWarning(ex, $"Error checking parent sample data flag for {typeof(TEntity).Name}"); + } + } + + /// + /// Checks if a parent entity has IsSampleData = true. + /// Simple direct query approach for better reliability and maintainability. + /// + /// Parent property name (e.g., "LeaseId", "PropertyId") + /// Parent entity ID + /// True if parent is sample data, false otherwise + private async Task CheckParentEntityIsSampleDataAsync(string parentPropertyName, Guid parentId) + { + try + { + // Direct queries for each entity type - simple and reliable + switch (parentPropertyName) + { + case "PropertyId": + var property = await _context.Properties + .Where(p => p.Id == parentId) + .Select(p => p.IsSampleData) + .FirstOrDefaultAsync(); + return property; + + case "LeaseId": + var lease = await _context.Leases + .Where(l => l.Id == parentId) + .Select(l => l.IsSampleData) + .FirstOrDefaultAsync(); + return lease; + + case "InvoiceId": + var invoice = await _context.Invoices + .Where(i => i.Id == parentId) + .Select(i => i.IsSampleData) + .FirstOrDefaultAsync(); + return invoice; + + case "TenantId": + var tenant = await _context.Tenants + .Where(t => t.Id == parentId) + .Select(t => t.IsSampleData) + .FirstOrDefaultAsync(); + return tenant; + + case "ProspectiveTenantId": + var prospect = await _context.ProspectiveTenants + .Where(pt => pt.Id == parentId) + .Select(pt => pt.IsSampleData) + .FirstOrDefaultAsync(); + return prospect; + + case "RentalApplicationId": + var application = await _context.RentalApplications + .Where(ra => ra.Id == parentId) + .Select(ra => ra.IsSampleData) + .FirstOrDefaultAsync(); + return application; + + case "RepairId": + var repair = await _context.Repairs + .Where(r => r.Id == parentId) + .Select(r => r.IsSampleData) + .FirstOrDefaultAsync(); + return repair; + + case "InspectionId": + var inspection = await _context.Inspections + .Where(i => i.Id == parentId) + .Select(i => i.IsSampleData) + .FirstOrDefaultAsync(); + return inspection; + + case "MaintenanceRequestId": + var maintenanceRequest = await _context.MaintenanceRequests + .Where(mr => mr.Id == parentId) + .Select(mr => mr.IsSampleData) + .FirstOrDefaultAsync(); + return maintenanceRequest; + + // OrganizationId is NOT checked - Organizations don't have IsSampleData flag + // Sample data is marked at the entity level within an organization + + default: + _logger.LogDebug($"Unknown parent property: {parentPropertyName}"); + return false; + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, $"Could not check IsSampleData for {parentPropertyName}"); + return false; + } + } + /// /// Hook method called after creating entity for post-creation operations. /// Override in derived services to handle side effects like updating related entities. diff --git a/2-Aquiis.Application/Services/CalendarEventService.cs b/2-Aquiis.Application/Services/CalendarEventService.cs index 573b6ee..cd00465 100644 --- a/2-Aquiis.Application/Services/CalendarEventService.cs +++ b/2-Aquiis.Application/Services/CalendarEventService.cs @@ -239,6 +239,12 @@ private CalendarEvent CreateEventFromEntity(T entity) where T : BaseModel, ISchedulableEntity { var eventType = entity.GetEventType(); + var propertyId = entity.GetPropertyId(); + + // Get property address for Location field + var property = propertyId.HasValue + ? _context.Properties.FirstOrDefault(p => p.Id == propertyId.Value) + : null; return new CalendarEvent { @@ -249,14 +255,16 @@ private CalendarEvent CreateEventFromEntity(T entity) EventType = eventType, Status = entity.GetEventStatus(), Description = entity.GetEventDescription(), - PropertyId = entity.GetPropertyId(), + PropertyId = propertyId, + Location = property?.Address ?? string.Empty, // Set location to property address Color = CalendarEventTypes.GetColor(eventType), Icon = CalendarEventTypes.GetIcon(eventType), SourceEntityId = entity.Id, SourceEntityType = typeof(T).Name, OrganizationId = entity.OrganizationId, CreatedBy = entity.CreatedBy, - CreatedOn = DateTime.UtcNow + CreatedOn = DateTime.UtcNow, + IsSampleData = entity.IsSampleData // Inherit sample data flag from entity }; } @@ -264,17 +272,26 @@ private CalendarEvent CreateEventFromEntity(T entity) /// Update a CalendarEvent from a schedulable entity /// private void UpdateEventFromEntity(CalendarEvent evt, T entity) - where T : ISchedulableEntity + where T : BaseModel, ISchedulableEntity { + var propertyId = entity.GetPropertyId(); + + // Get property address for Location field + var property = propertyId.HasValue + ? _context.Properties.FirstOrDefault(p => p.Id == propertyId.Value) + : null; + evt.Title = entity.GetEventTitle(); evt.StartOn = entity.GetEventStart(); evt.DurationMinutes = entity.GetEventDuration(); evt.EventType = entity.GetEventType(); evt.Status = entity.GetEventStatus(); evt.Description = entity.GetEventDescription(); - evt.PropertyId = entity.GetPropertyId(); + evt.PropertyId = propertyId; + evt.Location = property?.Address ?? string.Empty; // Update location to property address evt.Color = CalendarEventTypes.GetColor(entity.GetEventType()); evt.Icon = CalendarEventTypes.GetIcon(entity.GetEventType()); + evt.IsSampleData = entity.IsSampleData; // Inherit sample data flag from entity } } } diff --git a/2-Aquiis.Application/Services/DatabaseUnlockService.cs b/2-Aquiis.Application/Services/DatabaseUnlockService.cs index c1a13c6..cd21173 100644 --- a/2-Aquiis.Application/Services/DatabaseUnlockService.cs +++ b/2-Aquiis.Application/Services/DatabaseUnlockService.cs @@ -69,4 +69,41 @@ public DatabaseUnlockService( return (false, $"Error unlocking database: {ex.Message}"); } } + + /// + /// Archive encrypted database and create fresh database when password forgotten + /// + /// Path to encrypted database + /// (Success, ArchivedPath, ErrorMessage) + public async Task<(bool Success, string? ArchivedPath, string? ErrorMessage)> StartWithNewDatabaseAsync( + string databasePath) + { + try + { + _logger.LogWarning("User requested new database - archiving encrypted database"); + + // Create backups directory if it doesn't exist + var dbDirectory = Path.GetDirectoryName(databasePath)!; + var backupsDir = Path.Combine(dbDirectory, "Backups"); + Directory.CreateDirectory(backupsDir); + + // Generate archived filename with timestamp and .db extension for easy identification + var dbFileNameWithoutExt = Path.GetFileNameWithoutExtension(databasePath); + var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); + var archivedPath = Path.Combine(backupsDir, $"{dbFileNameWithoutExt}.{timestamp}.encrypted.db"); + + // Move encrypted database to backups + File.Move(databasePath, archivedPath); + _logger.LogInformation("Encrypted database archived to: {ArchivedPath}", archivedPath); + + // New unencrypted database will be created automatically on app restart + // The app will detect no database exists and go through first-time setup + return (true, archivedPath, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error archiving encrypted database"); + return (false, null, $"Error archiving database: {ex.Message}"); + } + } } diff --git a/2-Aquiis.Application/Services/InspectionService.cs b/2-Aquiis.Application/Services/InspectionService.cs index 8a20dfb..4e9360a 100644 --- a/2-Aquiis.Application/Services/InspectionService.cs +++ b/2-Aquiis.Application/Services/InspectionService.cs @@ -152,33 +152,26 @@ public async Task> GetByPropertyIdAsync(Guid propertyId) /// public override async Task CreateAsync(Inspection inspection) { - // Base validation and creation - await ValidateEntityAsync(inspection); - - var userId = await GetUserIdAsync(); - var organizationId = await GetActiveOrganizationIdAsync(); - - inspection.Id = Guid.NewGuid(); - inspection.OrganizationId = organizationId; - inspection.CreatedBy = userId; - inspection.CreatedOn = DateTime.UtcNow; - - await _context.Inspections.AddAsync(inspection); - await _context.SaveChangesAsync(); - - // Create calendar event for the inspection - await _calendarEventService.CreateOrUpdateEventAsync(inspection); - - // Update property inspection tracking if this is a routine inspection - if (inspection.InspectionType == ApplicationConstants.InspectionTypes.Routine) + // Call base.CreateAsync to handle: + // - Sample data propagation from Property + // - Organization context setup + // - Audit tracking fields + // - Validation + var createdInspection = await base.CreateAsync(inspection); + + // Custom logic: Create calendar event for the inspection + await _calendarEventService.CreateOrUpdateEventAsync(createdInspection); + + // Custom logic: Update property inspection tracking if this is a routine inspection + if (createdInspection.InspectionType == ApplicationConstants.InspectionTypes.Routine) { - await HandleRoutineInspectionCompletionAsync(inspection); + await HandleRoutineInspectionCompletionAsync(createdInspection); } _logger.LogInformation("Created inspection {InspectionId} for property {PropertyId}", - inspection.Id, inspection.PropertyId); + createdInspection.Id, createdInspection.PropertyId); - return inspection; + return createdInspection; } /// diff --git a/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs b/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs index 24bfc77..94c64f4 100644 --- a/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs +++ b/2-Aquiis.Application/Services/Workflows/SampleDataWorkflowService.cs @@ -120,31 +120,93 @@ public async Task RemoveSampleDataAsync() return WorkflowResult.Fail("Organization context not available."); } - var systemUserId = ApplicationConstants.SystemUser.Id; - _logger.LogInformation($"Removing sample data for Organization: {orgId}, CreatedBy: {systemUserId}"); + _logger.LogInformation($"Removing sample data for Organization: {orgId}"); - // Delete in reverse order of dependencies - var paymentsDeleted = await RemovePaymentsAsync(orgId, systemUserId); + // Delete in reverse hierarchical order (leaf nodes first, parents last) + // This prevents foreign key constraint violations + + var notificationsDeleted = await RemoveNotificationsAsync(orgId); + _logger.LogInformation($"Deleted {notificationsDeleted} notifications"); + + var notesDeleted = await RemoveNotesAsync(orgId); + _logger.LogInformation($"Deleted {notesDeleted} notes"); + + var checklistItemsDeleted = await RemoveChecklistItemsAsync(orgId); + _logger.LogInformation($"Deleted {checklistItemsDeleted} checklist items"); + + var checklistsDeleted = await RemoveChecklistsAsync(orgId); + _logger.LogInformation($"Deleted {checklistsDeleted} checklists"); + + var checklistTemplateItemsDeleted = await RemoveChecklistTemplateItemsAsync(orgId); + _logger.LogInformation($"Deleted {checklistTemplateItemsDeleted} checklist template items"); + + var checklistTemplatesDeleted = await RemoveChecklistTemplatesAsync(orgId); + _logger.LogInformation($"Deleted {checklistTemplatesDeleted} checklist templates"); + + var calendarEventsDeleted = await RemoveCalendarEventsAsync(orgId); + _logger.LogInformation($"Deleted {calendarEventsDeleted} calendar events"); + + var documentsDeleted = await RemoveDocumentsAsync(orgId); + _logger.LogInformation($"Deleted {documentsDeleted} documents"); + + + var paymentsDeleted = await RemovePaymentsAsync(orgId); _logger.LogInformation($"Deleted {paymentsDeleted} payments"); - - var invoicesDeleted = await RemoveInvoicesAsync(orgId, systemUserId); + + var invoicesDeleted = await RemoveInvoicesAsync(orgId); _logger.LogInformation($"Deleted {invoicesDeleted} invoices"); - - var leasesDeleted = await RemoveLeasesAsync(orgId, systemUserId); + + var repairsDeleted = await RemoveRepairsAsync(orgId); + _logger.LogInformation($"Deleted {repairsDeleted} repairs"); + + var maintenanceRequestsDeleted = await RemoveMaintenanceRequestsAsync(orgId); + _logger.LogInformation($"Deleted {maintenanceRequestsDeleted} maintenance requests"); + + var inspectionsDeleted = await RemoveInspectionsAsync(orgId); + _logger.LogInformation($"Deleted {inspectionsDeleted} inspections"); + + var toursDeleted = await RemoveToursAsync(orgId); + _logger.LogInformation($"Deleted {toursDeleted} tours"); + + var applicationScreeningsDeleted = await RemoveApplicationScreeningsAsync(orgId); + _logger.LogInformation($"Deleted {applicationScreeningsDeleted} application screenings"); + + var rentalApplicationsDeleted = await RemoveRentalApplicationsAsync(orgId); + _logger.LogInformation($"Deleted {rentalApplicationsDeleted} rental applications"); + + var prospectiveTenantsDeleted = await RemoveProspectiveTenantsAsync(orgId); + _logger.LogInformation($"Deleted {prospectiveTenantsDeleted} prospective tenants"); + + var leaseOffersDeleted = await RemoveLeaseOffersAsync(orgId); + _logger.LogInformation($"Deleted {leaseOffersDeleted} lease offers"); + + var securityDepositsDeleted = await RemoveSecurityDepositsAsync(orgId); + _logger.LogInformation($"Deleted {securityDepositsDeleted} security deposits"); + + var securityDepositDividendsDeleted = await RemoveSecurityDepositDividendsAsync(orgId); + _logger.LogInformation($"Deleted {securityDepositDividendsDeleted} security deposit dividends"); + + var securityDepositInvestmentPoolsDeleted = await RemoveSecurityDepositInvestmentPoolsAsync(orgId); + _logger.LogInformation($"Deleted {securityDepositInvestmentPoolsDeleted} security deposit investment pools"); + + var leasesDeleted = await RemoveLeasesAsync(orgId); _logger.LogInformation($"Deleted {leasesDeleted} leases"); - - var tenantsDeleted = await RemoveTenantsAsync(orgId, systemUserId); + + var tenantsDeleted = await RemoveTenantsAsync(orgId); _logger.LogInformation($"Deleted {tenantsDeleted} tenants"); - - var calendarEventsDeleted = await RemoveCalendarEventsAsync(orgId, systemUserId); - _logger.LogInformation($"Deleted {calendarEventsDeleted} calendar events"); - - var notificationsDeleted = await RemoveNotificationsAsync(orgId, systemUserId); - _logger.LogInformation($"Deleted {notificationsDeleted} notifications"); - - var propertiesDeleted = await RemovePropertiesAsync(orgId, systemUserId); + + var propertiesDeleted = await RemovePropertiesAsync(orgId); _logger.LogInformation($"Deleted {propertiesDeleted} properties"); + // Calculate total records deleted + var totalDeleted = propertiesDeleted + tenantsDeleted + leasesDeleted + invoicesDeleted + paymentsDeleted + + documentsDeleted + calendarEventsDeleted + notificationsDeleted + notesDeleted + + checklistItemsDeleted + checklistsDeleted + checklistTemplateItemsDeleted + checklistTemplatesDeleted + + repairsDeleted + maintenanceRequestsDeleted + inspectionsDeleted + toursDeleted + + applicationScreeningsDeleted + rentalApplicationsDeleted + prospectiveTenantsDeleted + + leaseOffersDeleted + securityDepositsDeleted + securityDepositDividendsDeleted + + securityDepositInvestmentPoolsDeleted; + // Log workflow completion await LogTransitionAsync( entityType: "SampleData", @@ -152,12 +214,15 @@ await LogTransitionAsync( fromStatus: "Generated", toStatus: "Removed", action: "RemoveSampleData", - reason: $"Deleted {propertiesDeleted} properties, {tenantsDeleted} tenants, {leasesDeleted} leases, {invoicesDeleted} invoices, {paymentsDeleted} payments, {calendarEventsDeleted} calendar events" + reason: $"Deleted {totalDeleted} total records across all entity types" ); return WorkflowResult.Ok( - $"Successfully removed sample data: {propertiesDeleted} properties, {tenantsDeleted} tenants, " + - $"{leasesDeleted} leases, {invoicesDeleted} invoices, {paymentsDeleted} payments, {calendarEventsDeleted} calendar events"); + $"Successfully removed {totalDeleted} sample data records: " + + $"{propertiesDeleted} properties, {tenantsDeleted} tenants, {leasesDeleted} leases, " + + $"{invoicesDeleted} invoices, {paymentsDeleted} payments, {documentsDeleted} documents, " + + $"{maintenanceRequestsDeleted} maintenance requests, {inspectionsDeleted} inspections, " + + $"{rentalApplicationsDeleted} applications, {prospectiveTenantsDeleted} prospects"); } catch (Exception ex) { @@ -174,7 +239,7 @@ private async Task> GeneratePropertiesAsync(Guid organizationId, var properties = new List(); var now = DateTime.UtcNow; - // Define 6 properties in Texas with varied characteristics + // Define 9 properties in Texas with varied characteristics var propertyData = new[] { new { Address = "1234 Riverside Dr", City = "Austin", State = "TX", Zip = "78701", Type = ApplicationConstants.PropertyTypes.House, Beds = 3, Baths = 2.0m, SqFt = 1850, Rent = 1850m, Status = ApplicationConstants.PropertyStatuses.Occupied }, @@ -182,7 +247,10 @@ private async Task> GeneratePropertiesAsync(Guid organizationId, new { Address = "910 Maple Ave", City = "Dallas", State = "TX", Zip = "75201", Type = ApplicationConstants.PropertyTypes.House, Beds = 4, Baths = 3.0m, SqFt = 2500, Rent = 2200m, Status = ApplicationConstants.PropertyStatuses.Occupied }, new { Address = "1122 Pine Ln", City = "San Antonio", State = "TX", Zip = "78205", Type = ApplicationConstants.PropertyTypes.Condo, Beds = 2, Baths = 1.0m, SqFt = 1100, Rent = 1200m, Status = ApplicationConstants.PropertyStatuses.Available }, new { Address = "3344 Elm Ct", City = "Fort Worth", State = "TX", Zip = "76102", Type = ApplicationConstants.PropertyTypes.House, Beds = 3, Baths = 2.0m, SqFt = 1750, Rent = 1750m, Status = ApplicationConstants.PropertyStatuses.Available }, - new { Address = "5566 Cedar Rd", City = "El Paso", State = "TX", Zip = "79901", Type = ApplicationConstants.PropertyTypes.Apartment, Beds = 1, Baths = 1.0m, SqFt = 850, Rent = 1100m, Status = ApplicationConstants.PropertyStatuses.Available } + new { Address = "5566 Cedar Rd", City = "El Paso", State = "TX", Zip = "79901", Type = ApplicationConstants.PropertyTypes.Apartment, Beds = 1, Baths = 1.0m, SqFt = 850, Rent = 1100m, Status = ApplicationConstants.PropertyStatuses.Available }, + new { Address = "7789 Bluebonnet Blvd", City = "Austin", State = "TX", Zip = "78758", Type = ApplicationConstants.PropertyTypes.House, Beds = 3, Baths = 2.5m, SqFt = 2000, Rent = 1950m, Status = ApplicationConstants.PropertyStatuses.Occupied }, + new { Address = "9012 Ranch Road", City = "Plano", State = "TX", Zip = "75024", Type = ApplicationConstants.PropertyTypes.Townhouse, Beds = 3, Baths = 2.5m, SqFt = 1650, Rent = 1800m, Status = ApplicationConstants.PropertyStatuses.Occupied }, + new { Address = "4455 Lonestar Dr", City = "Corpus Christi", State = "TX", Zip = "78401", Type = ApplicationConstants.PropertyTypes.Condo, Beds = 2, Baths = 2.0m, SqFt = 1300, Rent = 1350m, Status = ApplicationConstants.PropertyStatuses.Available } }; for (int i = 0; i < propertyData.Length; i++) @@ -233,12 +301,14 @@ private async Task> GenerateTenantsAsync(Guid organizationId, strin { var tenants = new List(); - // Define 3 tenants with realistic data + // Define 5 tenants with realistic data var tenantData = new[] { new { FirstName = "Sarah", LastName = "Johnson", DOB = new DateTime(1988, 5, 15), EmergencyName = "John Johnson", EmergencyPhone = "555-987-6543", Relationship = "Spouse" }, new { FirstName = "Michael", LastName = "Chen", DOB = new DateTime(1992, 8, 22), EmergencyName = "Lisa Chen", EmergencyPhone = "555-876-5432", Relationship = "Sister" }, - new { FirstName = "Emily", LastName = "Rodriguez", DOB = new DateTime(1990, 3, 10), EmergencyName = "Carlos Rodriguez", EmergencyPhone = "555-765-4321", Relationship = "Father" } + new { FirstName = "Emily", LastName = "Rodriguez", DOB = new DateTime(1990, 3, 10), EmergencyName = "Carlos Rodriguez", EmergencyPhone = "555-765-4321", Relationship = "Father" }, + new { FirstName = "James", LastName = "Martinez", DOB = new DateTime(1985, 11, 8), EmergencyName = "Maria Martinez", EmergencyPhone = "555-654-3210", Relationship = "Mother" }, + new { FirstName = "Amanda", LastName = "Williams", DOB = new DateTime(1993, 7, 25), EmergencyName = "Robert Williams", EmergencyPhone = "555-543-2109", Relationship = "Brother" } }; for (int i = 0; i < tenantData.Length; i++) @@ -287,8 +357,9 @@ private async Task> GenerateLeasesAsync( string userId) { var leases = new List(); + var currentDate = DateTime.UtcNow.Date; - // Create 3 leases for first 3 properties + // Create 3 standard leases for first 3 properties (original logic) var leaseStartMonths = new[] { 5, 6, 7 }; // May, June, July 2025 for (int i = 0; i < 3; i++) @@ -331,6 +402,63 @@ private async Task> GenerateLeasesAsync( property.IsAvailable = false; } + // Create 2 new leases expiring soon (properties 6 and 7, tenants 3 and 4) + // Lease 1: Expires in 30 days from current date + var endDate30 = currentDate.AddDays(30); + var startDate30 = endDate30.AddMonths(-11); // 12-month lease + var lease30Days = new Lease + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + PropertyId = properties[6].Id, // 7789 Bluebonnet Blvd + TenantId = tenants[3].Id, // James Martinez + StartDate = startDate30, + EndDate = endDate30, + MonthlyRent = properties[6].MonthlyRent, + SecurityDeposit = properties[6].MonthlyRent, + Status = ApplicationConstants.LeaseStatuses.Active, + Terms = $"12-month {ApplicationConstants.LeaseTypes.FixedTerm} lease. Rent: ${properties[6].MonthlyRent}/month. " + + $"Security Deposit: ${properties[6].MonthlyRent}. Payment due on the 5th of each month. EXPIRING SOON.", + SignedOn = startDate30.AddDays(-10), // Signed 10 days before start + OfferedOn = startDate30.AddDays(-20), // Offered 20 days before start + CreatedBy = userId, + CreatedOn = startDate30.AddDays(-25), + IsDeleted = false, + IsSampleData = true + }; + _context.Leases.Add(lease30Days); + leases.Add(lease30Days); + properties[6].Status = ApplicationConstants.PropertyStatuses.Occupied; + properties[6].IsAvailable = false; + + // Lease 2: Expires in 60 days from current date + var endDate60 = currentDate.AddDays(60); + var startDate60 = endDate60.AddMonths(-10); // 12-month lease + var lease60Days = new Lease + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + PropertyId = properties[7].Id, // 9012 Ranch Road + TenantId = tenants[4].Id, // Amanda Williams + StartDate = startDate60, + EndDate = endDate60, + MonthlyRent = properties[7].MonthlyRent, + SecurityDeposit = properties[7].MonthlyRent, + Status = ApplicationConstants.LeaseStatuses.Active, + Terms = $"12-month {ApplicationConstants.LeaseTypes.FixedTerm} lease. Rent: ${properties[7].MonthlyRent}/month. " + + $"Security Deposit: ${properties[7].MonthlyRent}. Payment due on the 5th of each month. EXPIRING SOON.", + SignedOn = startDate60.AddDays(-10), // Signed 10 days before start + OfferedOn = startDate60.AddDays(-20), // Offered 20 days before start + CreatedBy = userId, + CreatedOn = startDate60.AddDays(-25), + IsDeleted = false, + IsSampleData = true + }; + _context.Leases.Add(lease60Days); + leases.Add(lease60Days); + properties[7].Status = ApplicationConstants.PropertyStatuses.Occupied; + properties[7].IsAvailable = false; + await _context.SaveChangesAsync(); _logger.LogInformation($"Generated {leases.Count} leases"); @@ -415,73 +543,113 @@ private async Task> GeneratePaymentsAsync( var payments = new List(); var currentDate = DateTime.UtcNow.Date; - // Group invoices by lease to check remaining months + // Group invoices by lease var invoicesByLease = invoices.GroupBy(i => i.LeaseId).ToList(); foreach (var leaseGroup in invoicesByLease) { var leaseInvoices = leaseGroup.OrderBy(i => i.InvoicedOn).ToList(); - - // Calculate months remaining (leases end in May/June/July 2026) - var lastInvoice = leaseInvoices.Last(); var lease = await _context.Leases.FindAsync(leaseGroup.Key); if (lease == null) continue; - var monthsRemaining = ((lease.EndDate.Year - currentDate.Year) * 12) + - lease.EndDate.Month - currentDate.Month; + List invoicesToPay; - // If >3 months remaining, create payments for last 3 months only - if (monthsRemaining > 3) + // Check if this is one of the two new expiring leases by checking if lease has "EXPIRING SOON" in terms + if (lease.Terms != null && lease.Terms.Contains("EXPIRING SOON")) { - // Get last 3 invoices that have passed their due date - var recentInvoices = leaseInvoices + // New expiring leases: Pay ALL past-due invoices + invoicesToPay = leaseInvoices .Where(i => i.DueOn < currentDate) - .OrderByDescending(i => i.InvoicedOn) - .Take(3) .ToList(); + } + else + { + // Original 3 leases: Pay only last 3 invoices + var monthsRemaining = ((lease.EndDate.Year - currentDate.Year) * 12) + + lease.EndDate.Month - currentDate.Month; + + if (monthsRemaining > 3) + { + invoicesToPay = leaseInvoices + .Where(i => i.DueOn < currentDate) + .OrderByDescending(i => i.InvoicedOn) + .Take(3) + .ToList(); + } + else + { + invoicesToPay = new List(); + } + } - foreach (var invoice in recentInvoices) + foreach (var invoice in invoicesToPay) + { + // Random payment date logic: + // 70% chance: Pay before due date (between invoice date and due date) + // 30% chance: Pay late (1-10 days after due date) + DateTime paymentDate; + decimal lateFee = 0m; + + if (_random.Next(100) < 70) { - // Payment made 1-4 days before due date - var paymentDate = invoice.DueOn.AddDays(-_random.Next(1, 5)); - - // Generate proper payment number using service - var paymentNumber = await _paymentService.GeneratePaymentNumberAsync(); - - var payment = new Payment - { - Id = Guid.NewGuid(), - OrganizationId = organizationId, - InvoiceId = invoice.Id, - Amount = invoice.Amount, - PaymentNumber = paymentNumber, - PaidOn = paymentDate, - PaymentMethod = GetRandomPaymentMethod(), - Notes = $"Payment for {invoice.Description}", - CreatedBy = userId, - CreatedOn = paymentDate, - IsDeleted = false, - IsSampleData = true - }; - - _context.Payments.Add(payment); - - // CRITICAL FIX: Save immediately after generating number to prevent collisions - await _context.SaveChangesAsync(); - - payments.Add(payment); - - // Update invoice status to Paid - invoice.Status = ApplicationConstants.InvoiceStatuses.Paid; - invoice.AmountPaid = invoice.Amount; - invoice.PaidOn = paymentDate; - invoice.LastModifiedBy = userId; - invoice.LastModifiedOn = paymentDate; - - // Save invoice status update immediately - await _context.SaveChangesAsync(); + // Pay on time: Random date between invoice date and due date + var daysInPeriod = (invoice.DueOn - invoice.InvoicedOn).Days; + var randomDays = _random.Next(daysInPeriod + 1); + paymentDate = invoice.InvoicedOn.AddDays(randomDays); } + else + { + // Pay late: 1-10 days after due date + var lateDays = _random.Next(1, 11); + paymentDate = invoice.DueOn.AddDays(lateDays); + lateFee = 50m; // $50 late fee + } + + // Ensure payment date doesn't exceed current date + if (paymentDate > currentDate) + paymentDate = currentDate; + + // Calculate total payment amount (invoice + late fee if applicable) + var totalAmount = invoice.Amount + lateFee; + + // Generate proper payment number using service + var paymentNumber = await _paymentService.GeneratePaymentNumberAsync(); + + var payment = new Payment + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + InvoiceId = invoice.Id, + Amount = totalAmount, + PaymentNumber = paymentNumber, + PaidOn = paymentDate, + PaymentMethod = GetRandomPaymentMethod(), + Notes = lateFee > 0 + ? $"Payment for {invoice.Description} (includes $50 late fee)" + : $"Payment for {invoice.Description}", + CreatedBy = userId, + CreatedOn = paymentDate, + IsDeleted = false, + IsSampleData = true + }; + + _context.Payments.Add(payment); + + // CRITICAL FIX: Save immediately after generating number to prevent collisions + await _context.SaveChangesAsync(); + + payments.Add(payment); + + // Update invoice status to Paid + invoice.Status = ApplicationConstants.InvoiceStatuses.Paid; + invoice.AmountPaid = totalAmount; + invoice.PaidOn = paymentDate; + invoice.LastModifiedBy = userId; + invoice.LastModifiedOn = paymentDate; + + // Save invoice status update immediately + await _context.SaveChangesAsync(); } } @@ -602,7 +770,7 @@ private string GetRandomPaymentMethod() #region Sample Data Removal - private async Task RemovePropertiesAsync(Guid organizationId, string systemUserId) + private async Task RemovePropertiesAsync(Guid organizationId) { var properties = await _context.Properties .Where(p => p.OrganizationId == organizationId && p.IsSampleData) @@ -614,7 +782,7 @@ private async Task RemovePropertiesAsync(Guid organizationId, string system return properties.Count; } - private async Task RemoveTenantsAsync(Guid organizationId, string systemUserId) + private async Task RemoveTenantsAsync(Guid organizationId) { var tenants = await _context.Tenants .Where(t => t.OrganizationId == organizationId && t.IsSampleData) @@ -626,7 +794,7 @@ private async Task RemoveTenantsAsync(Guid organizationId, string systemUse return tenants.Count; } - private async Task RemoveLeasesAsync(Guid organizationId, string systemUserId) + private async Task RemoveLeasesAsync(Guid organizationId) { var leases = await _context.Leases .Where(l => l.OrganizationId == organizationId && l.IsSampleData) @@ -638,7 +806,7 @@ private async Task RemoveLeasesAsync(Guid organizationId, string systemUser return leases.Count; } - private async Task RemoveInvoicesAsync(Guid organizationId, string systemUserId) + private async Task RemoveInvoicesAsync(Guid organizationId) { var invoices = await _context.Invoices .Where(i => i.OrganizationId == organizationId && i.IsSampleData) @@ -650,7 +818,7 @@ private async Task RemoveInvoicesAsync(Guid organizationId, string systemUs return invoices.Count; } - private async Task RemovePaymentsAsync(Guid organizationId, string systemUserId) + private async Task RemovePaymentsAsync(Guid organizationId) { var payments = await _context.Payments .Where(p => p.OrganizationId == organizationId && p.IsSampleData) @@ -662,7 +830,211 @@ private async Task RemovePaymentsAsync(Guid organizationId, string systemUs return payments.Count; } - private async Task RemoveCalendarEventsAsync(Guid organizationId, string systemUserId) + private async Task RemoveDocumentsAsync(Guid organizationId) + { + var documents = await _context.Documents + .Where(d => d.OrganizationId == organizationId && d.IsSampleData) + .ToListAsync(); + + _context.Documents.RemoveRange(documents); + await _context.SaveChangesAsync(); + + return documents.Count; + } + + private async Task RemoveNotesAsync(Guid organizationId) + { + var notes = await _context.Notes + .Where(n => n.OrganizationId == organizationId && n.IsSampleData) + .ToListAsync(); + + _context.Notes.RemoveRange(notes); + await _context.SaveChangesAsync(); + + return notes.Count; + } + + private async Task RemoveChecklistItemsAsync(Guid organizationId) + { + var checklistItems = await _context.ChecklistItems + .Where(ci => ci.OrganizationId == organizationId && ci.IsSampleData) + .ToListAsync(); + + _context.ChecklistItems.RemoveRange(checklistItems); + await _context.SaveChangesAsync(); + + return checklistItems.Count; + } + + private async Task RemoveChecklistsAsync(Guid organizationId) + { + var checklists = await _context.Checklists + .Where(c => c.OrganizationId == organizationId && c.IsSampleData) + .ToListAsync(); + + _context.Checklists.RemoveRange(checklists); + await _context.SaveChangesAsync(); + + return checklists.Count; + } + + private async Task RemoveChecklistTemplateItemsAsync(Guid organizationId) + { + var checklistTemplateItems = await _context.ChecklistTemplateItems + .Where(cti => cti.OrganizationId == organizationId && cti.IsSampleData) + .ToListAsync(); + + _context.ChecklistTemplateItems.RemoveRange(checklistTemplateItems); + await _context.SaveChangesAsync(); + + return checklistTemplateItems.Count; + } + + private async Task RemoveChecklistTemplatesAsync(Guid organizationId) + { + var checklistTemplates = await _context.ChecklistTemplates + .Where(ct => ct.OrganizationId == organizationId && ct.IsSampleData) + .ToListAsync(); + + _context.ChecklistTemplates.RemoveRange(checklistTemplates); + await _context.SaveChangesAsync(); + + return checklistTemplates.Count; + } + + private async Task RemoveRepairsAsync(Guid organizationId) + { + var repairs = await _context.Repairs + .Where(r => r.OrganizationId == organizationId && r.IsSampleData) + .ToListAsync(); + + _context.Repairs.RemoveRange(repairs); + await _context.SaveChangesAsync(); + + return repairs.Count; + } + + private async Task RemoveMaintenanceRequestsAsync(Guid organizationId) + { + var maintenanceRequests = await _context.MaintenanceRequests + .Where(mr => mr.OrganizationId == organizationId && mr.IsSampleData) + .ToListAsync(); + + _context.MaintenanceRequests.RemoveRange(maintenanceRequests); + await _context.SaveChangesAsync(); + + return maintenanceRequests.Count; + } + + private async Task RemoveInspectionsAsync(Guid organizationId) + { + var inspections = await _context.Inspections + .Where(i => i.OrganizationId == organizationId && i.IsSampleData) + .ToListAsync(); + + _context.Inspections.RemoveRange(inspections); + await _context.SaveChangesAsync(); + + return inspections.Count; + } + + private async Task RemoveToursAsync(Guid organizationId) + { + var tours = await _context.Tours + .Where(t => t.OrganizationId == organizationId && t.IsSampleData) + .ToListAsync(); + + _context.Tours.RemoveRange(tours); + await _context.SaveChangesAsync(); + + return tours.Count; + } + + private async Task RemoveApplicationScreeningsAsync(Guid organizationId) + { + var applicationScreenings = await _context.ApplicationScreenings + .Where(a => a.OrganizationId == organizationId && a.IsSampleData) + .ToListAsync(); + + _context.ApplicationScreenings.RemoveRange(applicationScreenings); + await _context.SaveChangesAsync(); + + return applicationScreenings.Count; + } + + private async Task RemoveRentalApplicationsAsync(Guid organizationId) + { + var rentalApplications = await _context.RentalApplications + .Where(ra => ra.OrganizationId == organizationId && ra.IsSampleData) + .ToListAsync(); + + _context.RentalApplications.RemoveRange(rentalApplications); + await _context.SaveChangesAsync(); + + return rentalApplications.Count; + } + + private async Task RemoveProspectiveTenantsAsync(Guid organizationId) + { + var prospectiveTenants = await _context.ProspectiveTenants + .Where(pt => pt.OrganizationId == organizationId && pt.IsSampleData) + .ToListAsync(); + + _context.ProspectiveTenants.RemoveRange(prospectiveTenants); + await _context.SaveChangesAsync(); + + return prospectiveTenants.Count; + } + + private async Task RemoveLeaseOffersAsync(Guid organizationId) + { + var leaseOffers = await _context.LeaseOffers + .Where(lo => lo.OrganizationId == organizationId && lo.IsSampleData) + .ToListAsync(); + + _context.LeaseOffers.RemoveRange(leaseOffers); + await _context.SaveChangesAsync(); + + return leaseOffers.Count; + } + + private async Task RemoveSecurityDepositsAsync(Guid organizationId) + { + var securityDeposits = await _context.SecurityDeposits + .Where(sd => sd.OrganizationId == organizationId && sd.IsSampleData) + .ToListAsync(); + + _context.SecurityDeposits.RemoveRange(securityDeposits); + await _context.SaveChangesAsync(); + + return securityDeposits.Count; + } + + private async Task RemoveSecurityDepositDividendsAsync(Guid organizationId) + { + var securityDepositDividends = await _context.SecurityDepositDividends + .Where(sdd => sdd.OrganizationId == organizationId && sdd.IsSampleData) + .ToListAsync(); + + _context.SecurityDepositDividends.RemoveRange(securityDepositDividends); + await _context.SaveChangesAsync(); + + return securityDepositDividends.Count; + } + + private async Task RemoveSecurityDepositInvestmentPoolsAsync(Guid organizationId) + { + var securityDepositInvestmentPools = await _context.SecurityDepositInvestmentPools + .Where(sdip => sdip.OrganizationId == organizationId && sdip.IsSampleData) + .ToListAsync(); + + _context.SecurityDepositInvestmentPools.RemoveRange(securityDepositInvestmentPools); + await _context.SaveChangesAsync(); + + return securityDepositInvestmentPools.Count; + } + + private async Task RemoveCalendarEventsAsync(Guid organizationId) { var calendarEvents = await _context.CalendarEvents .Where(ce => ce.OrganizationId == organizationId && ce.IsSampleData) @@ -674,7 +1046,7 @@ private async Task RemoveCalendarEventsAsync(Guid organizationId, string sy return calendarEvents.Count; } - private async Task RemoveNotificationsAsync(Guid organizationId, string systemUserId) + private async Task RemoveNotificationsAsync(Guid organizationId) { var notifications = await _context.Notifications .Where(n => n.OrganizationId == organizationId && n.IsSampleData) @@ -760,6 +1132,7 @@ public async Task GetSampleDataSummaryAsync() LeaseCount = await _context.Leases.CountAsync(l => l.OrganizationId == orgId && l.IsSampleData && !l.IsDeleted), InvoiceCount = await _context.Invoices.CountAsync(i => i.OrganizationId == orgId && i.IsSampleData && !i.IsDeleted), PaymentCount = await _context.Payments.CountAsync(p => p.OrganizationId == orgId && p.IsSampleData && !p.IsDeleted), + DocumentCount = await _context.Documents.CountAsync(d => d.OrganizationId == orgId && d.IsSampleData && !d.IsDeleted), CalendarEventCount = await _context.CalendarEvents.CountAsync(ce => ce.OrganizationId == orgId && ce.IsSampleData && !ce.IsDeleted) }; } @@ -777,10 +1150,11 @@ public class SampleDataSummary public int LeaseCount { get; set; } public int InvoiceCount { get; set; } public int PaymentCount { get; set; } + public int DocumentCount { get; set; } public int CalendarEventCount { get; set; } public int NotificationCount { get; set; } - public int TotalCount => PropertyCount + TenantCount + LeaseCount + InvoiceCount + PaymentCount + CalendarEventCount + NotificationCount; + public int TotalCount => PropertyCount + TenantCount + LeaseCount + InvoiceCount + PaymentCount + DocumentCount + CalendarEventCount + NotificationCount; public bool HasData => TotalCount > 0; } } diff --git a/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseRenewalList.razor b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseRenewalList.razor index 26b938b..8451ee7 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseRenewalList.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseRenewalList.razor @@ -88,7 +88,7 @@ } @@ -112,6 +112,7 @@ private bool isLoading = true; private int selectedFilter = 60; + [Parameter] public EventCallback OnViewAll { get; set; } diff --git a/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor b/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor index d9ff52c..49a8272 100644 --- a/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor +++ b/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor @@ -320,6 +320,7 @@ else .notification-bell { position: relative; display: inline-block; + z-index: 10000; } .notification-bell-button { @@ -332,10 +333,12 @@ else color: var(--bs-body-color); transition: all 0.2s; white-space: nowrap; + z-index: 10000; } .notification-bell-button:hover { background-color: var(--bs-secondary-bg); + z-index: 10000; } .notification-bell-icon { @@ -368,6 +371,7 @@ else .notification-dropdown { position: relative; + z-index: 9999; } .notification-dropdown .dropdown-menu { @@ -378,28 +382,34 @@ else min-width: 320px; max-height: 400px; overflow-y: auto; + z-index: 10000; } .notification-dropdown .dropdown-item { padding: 0.75rem 1rem; border-bottom: 1px solid var(--bs-border-color); + z-index: 10000; } .notification-dropdown .dropdown-item:last-child { border-bottom: none; + z-index: 10000; } .notification-dropdown .dropdown-item a { color: var(--bs-body-color); text-decoration: none; cursor: pointer; + z-index: 10000; } .notification-dropdown .dropdown-item a:hover { text-decoration: underline; + z-index: 10000; } .notification-dropdown .dropdown-item i { margin-right: 0.5rem; + z-index: 10000; } \ No newline at end of file diff --git a/4-Aquiis.SimpleStart/Features/DatabaseUnlock/Index.razor b/4-Aquiis.SimpleStart/Features/DatabaseUnlock/Index.razor index ff273ec..2aded48 100644 --- a/4-Aquiis.SimpleStart/Features/DatabaseUnlock/Index.razor +++ b/4-Aquiis.SimpleStart/Features/DatabaseUnlock/Index.razor @@ -1,6 +1,7 @@ @page "/database-unlock" @using Aquiis.Infrastructure.Services @using Aquiis.Application.Services +@using ElectronNET.API @inject DatabaseUnlockState UnlockState @inject DatabaseUnlockService UnlockService @inject NavigationManager Navigation @@ -67,6 +68,22 @@ + +
+
+
+ Can't remember your password? +
+
+ + + +
@@ -107,6 +124,68 @@
+ +@if (showStartFreshModal) +{ + +} +