diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e3db819 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "9.0.x" + + - name: Restore + run: dotnet restore Aquiis.sln + + - name: Build + run: dotnet build Aquiis.sln --no-restore --configuration Release + + - name: Run focused tests + run: dotnet test Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj --no-build --verbosity normal diff --git a/.gitignore b/.gitignore index 0f68e8d..aa4f4f9 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ Data/Backups/** /Data/app* + +# Python virtual environment created per-project +.venv/ + diff --git a/Aquiis.SimpleStart.Tests/ApplicationWorkflowService.LeaseLifecycleTests.cs b/Aquiis.SimpleStart.Tests/ApplicationWorkflowService.LeaseLifecycleTests.cs new file mode 100644 index 0000000..2e98f24 --- /dev/null +++ b/Aquiis.SimpleStart.Tests/ApplicationWorkflowService.LeaseLifecycleTests.cs @@ -0,0 +1,147 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using System; +using Aquiis.SimpleStart.Application.Services.Workflows; +using Aquiis.SimpleStart.Core.Entities; +using Aquiis.SimpleStart.Shared.Services; +using Aquiis.SimpleStart.Shared.Components.Account; +using Aquiis.SimpleStart.Core.Constants; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Aquiis.SimpleStart.Tests; + +public class ApplicationWorkflowServiceLeaseLifecycleTests +{ + [Fact] + public async Task GenerateAndAcceptLeaseOffer_CreatesLeaseAndTenant_UpdatesProperty() + { + // Arrange - setup SQLite in-memory + var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); + connection.Open(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + var testUserId = "test-user-id"; + var orgId = Guid.NewGuid(); + + // Mock AuthenticationStateProvider + var claims = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, testUserId) }, "TestAuth")); + var mockAuth = new Mock(); + mockAuth.Setup(a => a.GetAuthenticationStateAsync()) + .ReturnsAsync(new AuthenticationState(claims)); + + var mockUserStore = new Mock>(); + var mockUserManager = new Mock>( + mockUserStore.Object, + null, null, null, null, null, null, null, null); + + var appUser = new ApplicationUser { Id = testUserId, ActiveOrganizationId = orgId }; + mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())).ReturnsAsync(appUser); + + var serviceProvider = new Mock(); + + var userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); + + // Create DbContext and seed data + await using var context = new Infrastructure.Data.ApplicationDbContext(options); + await context.Database.EnsureCreatedAsync(); + + var appUserEntity = new ApplicationUser { Id = testUserId, UserName = "testuser", Email = "t@t.com", ActiveOrganizationId = orgId }; + context.Users.Add(appUserEntity); + + var org = new Organization { Id = orgId, Name = "TestOrg", OwnerId = testUserId, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; + context.Organizations.Add(org); + + var prospect = new ProspectiveTenant { Id = Guid.NewGuid(), OrganizationId = orgId, FirstName = "Lease", LastName = "Tester", Email = "lt@example.com", Phone = "123", Status = ApplicationConstants.ProspectiveStatuses.Lead, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; + var property = new Property { Id = Guid.NewGuid(), OrganizationId = orgId, Address = "456 Elm", City = "X", State = "ST", ZipCode = "00000", Status = ApplicationConstants.PropertyStatuses.Available, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow, MonthlyRent = 1200m }; + context.ProspectiveTenants.Add(prospect); + context.Properties.Add(property); + await context.SaveChangesAsync(); + + var noteService = new Application.Services.NoteService(context, userContext); + var workflowService = new ApplicationWorkflowService(context, userContext, noteService); + + // Submit application + var submissionModel = new ApplicationSubmissionModel + { + ApplicationFee = 25m, + ApplicationFeePaid = true, + ApplicationFeePaymentMethod = "Card", + CurrentAddress = "Addr", + CurrentCity = "C", + CurrentState = "ST", + CurrentZipCode = "00000", + CurrentRent = 1000m, + LandlordName = "L", + LandlordPhone = "P", + EmployerName = "E", + JobTitle = "J", + MonthlyIncome = 2000m, + EmploymentLengthMonths = 12, + Reference1Name = "R1", + Reference1Phone = "111", + Reference1Relationship = "Friend" + }; + + var submitResult = await workflowService.SubmitApplicationAsync(prospect.Id, property.Id, submissionModel); + Assert.True(submitResult.Success, string.Join(";", submitResult.Errors)); + var application = submitResult.Data!; + + // Initiate screening and complete it as Passed + var screeningResult = await workflowService.InitiateScreeningAsync(application.Id, true, true); + Assert.True(screeningResult.Success, string.Join(";", screeningResult.Errors)); + + var completeScreeningResult = await workflowService.CompleteScreeningAsync(application.Id, new ScreeningResultModel + { + BackgroundCheckPassed = true, + CreditCheckPassed = true, + CreditScore = 700, + OverallResult = "Passed", + ResultNotes = "All good" + }); + Assert.True(completeScreeningResult.Success, string.Join(";", completeScreeningResult.Errors)); + + // Approve application + var approveResult = await workflowService.ApproveApplicationAsync(application.Id); + Assert.True(approveResult.Success, string.Join(";", approveResult.Errors)); + + // Generate lease offer + var offerModel = new LeaseOfferModel + { + StartDate = DateTime.Today.AddDays(14), + EndDate = DateTime.Today.AddYears(1).AddDays(14), + MonthlyRent = property.MonthlyRent, + SecurityDeposit = property.MonthlyRent, + Terms = "Standard", + Notes = "Test offer" + }; + + var generateResult = await workflowService.GenerateLeaseOfferAsync(application.Id, offerModel); + Assert.True(generateResult.Success, string.Join(";", generateResult.Errors)); + var leaseOffer = generateResult.Data!; + + // Accept lease offer + var acceptResult = await workflowService.AcceptLeaseOfferAsync(leaseOffer.Id, "Card", DateTime.UtcNow); + Assert.True(acceptResult.Success, string.Join(";", acceptResult.Errors)); + var lease = acceptResult.Data!; + + // Assert: Lease exists in DB, Tenant created, Property status Occupied + var dbLease = await context.Leases.Include(l => l.Tenant).FirstOrDefaultAsync(l => l.Id == lease.Id); + Assert.NotNull(dbLease); + Assert.NotNull(dbLease!.Tenant); + + var dbProperty = await context.Properties.FirstOrDefaultAsync(p => p.Id == property.Id); + Assert.NotNull(dbProperty); + Assert.Equal(ApplicationConstants.PropertyStatuses.Occupied, dbProperty!.Status); + + // Audit logs should contain LeaseOffer Accept entry + var audit = await context.WorkflowAuditLogs.FirstOrDefaultAsync(w => w.EntityType == "LeaseOffer" && w.EntityId == leaseOffer.Id); + Assert.NotNull(audit); + } +} diff --git a/Aquiis.SimpleStart.Tests/ApplicationWorkflowServiceTests.cs b/Aquiis.SimpleStart.Tests/ApplicationWorkflowServiceTests.cs new file mode 100644 index 0000000..a674279 --- /dev/null +++ b/Aquiis.SimpleStart.Tests/ApplicationWorkflowServiceTests.cs @@ -0,0 +1,116 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using System; +using Aquiis.SimpleStart.Application.Services.Workflows; +using Aquiis.SimpleStart.Core.Entities; +using Aquiis.SimpleStart.Shared.Services; +using Aquiis.SimpleStart.Shared.Components.Account; +using Aquiis.SimpleStart.Core.Constants; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Aquiis.SimpleStart.Tests; + +public class ApplicationWorkflowServiceTests +{ + [Fact] + public async Task GetApplicationWorkflowStateAsync_ReturnsExpectedState() + { + // Arrange + // Use SQLite in-memory to support transactions used by workflow base class + var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); + connection.Open(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + // Create test user and org + var testUserId = "test-user-id"; + var orgId = Guid.NewGuid(); + + // Mock AuthenticationStateProvider + var claims = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, testUserId) }, "TestAuth")); + var mockAuth = new Mock(); + mockAuth.Setup(a => a.GetAuthenticationStateAsync()) + .ReturnsAsync(new AuthenticationState(claims)); + + // Mock UserManager to return an ApplicationUser with ActiveOrganizationId set + var mockUserStore = new Mock>(); + var mockUserManager = new Mock>( + mockUserStore.Object, + null, null, null, null, null, null, null, null); + + var appUser = new ApplicationUser { Id = testUserId, ActiveOrganizationId = orgId }; + mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())).ReturnsAsync(appUser); + + var serviceProvider = new Mock(); + + // Create real UserContextService using mocks + var userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); + + // Create DbContext and seed prospect/property + await using var context = new Infrastructure.Data.ApplicationDbContext(options); + // Ensure schema is created for SQLite in-memory + await context.Database.EnsureCreatedAsync(); + var appUserEntity = new ApplicationUser { Id = testUserId, UserName = "testuser", Email = "t@t.com", ActiveOrganizationId = orgId }; + context.Users.Add(appUserEntity); + + var org = new Organization { Id = orgId, Name = "TestOrg", OwnerId = testUserId, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; + context.Organizations.Add(org); + + var prospect = new ProspectiveTenant { Id = Guid.NewGuid(), OrganizationId = orgId, FirstName = "Test", LastName = "User", Email = "t@t.com", Phone = "123", Status = "Lead", CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; + var property = new Property { Id = Guid.NewGuid(), OrganizationId = orgId, Address = "123 Main", City = "X", State = "ST", ZipCode = "00000", Status = ApplicationConstants.PropertyStatuses.Available, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; + context.ProspectiveTenants.Add(prospect); + context.Properties.Add(property); + await context.SaveChangesAsync(); + + // Create NoteService (not used heavily in this test) + var noteService = new Application.Services.NoteService(context, userContext); + + var workflowService = new ApplicationWorkflowService(context, userContext, noteService); + + // Act - submit application then initiate screening + var submissionModel = new ApplicationSubmissionModel + { + ApplicationFee = 25m, + ApplicationFeePaid = true, + ApplicationFeePaymentMethod = "Card", + CurrentAddress = "Addr", + CurrentCity = "C", + CurrentState = "ST", + CurrentZipCode = "00000", + CurrentRent = 1000m, + LandlordName = "L", + LandlordPhone = "P", + EmployerName = "E", + JobTitle = "J", + MonthlyIncome = 2000m, + EmploymentLengthMonths = 12, + Reference1Name = "R1", + Reference1Phone = "111", + Reference1Relationship = "Friend" + }; + + var submitResult = await workflowService.SubmitApplicationAsync(prospect.Id, property.Id, submissionModel); + Assert.True(submitResult.Success, string.Join(";", submitResult.Errors)); + + var application = submitResult.Data!; + + var screeningResult = await workflowService.InitiateScreeningAsync(application.Id, true, true); + Assert.True(screeningResult.Success, string.Join(";", screeningResult.Errors)); + + // Get aggregated workflow state + var state = await workflowService.GetApplicationWorkflowStateAsync(application.Id); + + // Assert + Assert.NotNull(state.Application); + Assert.NotNull(state.Prospect); + Assert.NotNull(state.Property); + Assert.NotNull(state.Screening); + Assert.NotEmpty(state.AuditHistory); + + } +} diff --git a/Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj b/Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj new file mode 100644 index 0000000..656978d --- /dev/null +++ b/Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + false + enable + + + + + + + + + + + + + + + diff --git a/Aquiis.SimpleStart/.github/copilot-instructions.md b/Aquiis.SimpleStart/.github/copilot-instructions.md index aefdf5c..95ca41a 100644 --- a/Aquiis.SimpleStart/.github/copilot-instructions.md +++ b/Aquiis.SimpleStart/.github/copilot-instructions.md @@ -1,5 +1,43 @@ # Aquiis Property Management System - AI Agent Instructions +## Development Workflow + +### Feature Branch Strategy + +**CRITICAL: Always develop new features on feature branches, never directly on main** + +1. **Create Feature Branch**: Before starting any new phase or major feature: + ```bash + git checkout -b Phase-X-Feature-Name + ``` + - Use descriptive names: `Phase-6-Workflow-Services-and-Automation` + - Branch from `main` to ensure clean starting point + +2. **Development on Feature Branch**: + - All commits for the feature go to the feature branch + - Build and test frequently to ensure no breaking changes + - Keep commits focused and atomic + +3. **Merge to Main** (only when complete and error-free): + ```bash + # Ensure build succeeds with 0 errors + dotnet build Aquiis.sln + + # Switch to main and merge + git checkout main + git merge Phase-X-Feature-Name + + # Push to remote + git push origin main + ``` + +4. **Protection**: Main branch should always be production-ready + - Never commit directly to main during active development + - Only merge tested, working code + - If build fails, fix on feature branch before merging + +--- + ## Project Overview Aquiis is a multi-tenant property management system built with **ASP.NET Core 9.0 + Blazor Server**. It manages properties, tenants, leases, invoices, payments, documents, inspections, and maintenance requests with role-based access control. diff --git a/Aquiis.SimpleStart/Application/Services/CalendarEventService.cs b/Aquiis.SimpleStart/Application/Services/CalendarEventService.cs index 41926db..b5a92e9 100644 --- a/Aquiis.SimpleStart/Application/Services/CalendarEventService.cs +++ b/Aquiis.SimpleStart/Application/Services/CalendarEventService.cs @@ -88,7 +88,7 @@ public CalendarEventService(ApplicationDbContext context, CalendarSettingsServic /// /// Delete a calendar event /// - public async Task DeleteEventAsync(int? calendarEventId) + public async Task DeleteEventAsync(Guid? calendarEventId) { if (!calendarEventId.HasValue) return; @@ -127,7 +127,7 @@ public async Task> GetEventsAsync( /// /// Get a specific calendar event by ID /// - public async Task GetEventByIdAsync(int eventId) + public async Task GetEventByIdAsync(Guid eventId) { var organizationId = await _userContextService.GetActiveOrganizationIdAsync(); return await _context.CalendarEvents @@ -187,7 +187,7 @@ public async Task CreateCustomEventAsync(CalendarEvent calendarEv /// /// Get all calendar events for a specific property /// - public async Task> GetEventsByPropertyIdAsync(int propertyId) + public async Task> GetEventsByPropertyIdAsync(Guid propertyId) { var organizationId = await _userContextService.GetActiveOrganizationIdAsync(); return await _context.CalendarEvents @@ -221,6 +221,7 @@ private CalendarEvent CreateEventFromEntity(T entity) return new CalendarEvent { + Id = Guid.NewGuid(), Title = entity.GetEventTitle(), StartOn = entity.GetEventStart(), DurationMinutes = entity.GetEventDuration(), diff --git a/Aquiis.SimpleStart/Application/Services/CalendarSettingsService.cs b/Aquiis.SimpleStart/Application/Services/CalendarSettingsService.cs index ed75e3f..8780c58 100644 --- a/Aquiis.SimpleStart/Application/Services/CalendarSettingsService.cs +++ b/Aquiis.SimpleStart/Application/Services/CalendarSettingsService.cs @@ -17,7 +17,7 @@ public CalendarSettingsService(ApplicationDbContext context, UserContextService _userContext = userContext; } - public async Task> GetSettingsAsync(string organizationId) + public async Task> GetSettingsAsync(Guid organizationId) { await EnsureDefaultsAsync(organizationId); @@ -28,7 +28,7 @@ public async Task> GetSettingsAsync(string organizationId .ToListAsync(); } - public async Task GetSettingAsync(string organizationId, string entityType) + public async Task GetSettingAsync(Guid organizationId, string entityType) { var setting = await _context.CalendarSettings .FirstOrDefaultAsync(s => s.OrganizationId == organizationId @@ -58,7 +58,7 @@ public async Task UpdateSettingAsync(CalendarSettings setting) return setting; } - public async Task IsAutoCreateEnabledAsync(string organizationId, string entityType) + public async Task IsAutoCreateEnabledAsync(Guid organizationId, string entityType) { var setting = await _context.CalendarSettings .FirstOrDefaultAsync(s => s.OrganizationId == organizationId @@ -69,7 +69,7 @@ public async Task IsAutoCreateEnabledAsync(string organizationId, string e return setting?.AutoCreateEvents ?? true; } - public async Task EnsureDefaultsAsync(string organizationId) + public async Task EnsureDefaultsAsync(Guid organizationId) { var userId = await _userContext.GetUserIdAsync(); var entityTypes = SchedulableEntityRegistry.GetEntityTypeNames(); @@ -96,7 +96,7 @@ public async Task EnsureDefaultsAsync(string organizationId) } } - private CalendarSettings CreateDefaultSetting(string organizationId, string entityType) + private CalendarSettings CreateDefaultSetting(Guid organizationId, string entityType) { // Get defaults from CalendarEventTypes if available var config = CalendarEventTypes.Config.ContainsKey(entityType) @@ -106,6 +106,7 @@ private CalendarSettings CreateDefaultSetting(string organizationId, string enti var userId = _userContext.GetUserIdAsync().Result; return new CalendarSettings { + Id = Guid.NewGuid(), OrganizationId = organizationId, EntityType = entityType, AutoCreateEvents = true, diff --git a/Aquiis.SimpleStart/Application/Services/ChecklistService.cs b/Aquiis.SimpleStart/Application/Services/ChecklistService.cs index 26b5f3f..a46be58 100644 --- a/Aquiis.SimpleStart/Application/Services/ChecklistService.cs +++ b/Aquiis.SimpleStart/Application/Services/ChecklistService.cs @@ -41,7 +41,7 @@ public async Task> GetChecklistTemplatesAsync() .ToListAsync(); } - public async Task GetChecklistTemplateByIdAsync(int templateId) + public async Task GetChecklistTemplateByIdAsync(Guid templateId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -78,7 +78,7 @@ public async Task AddChecklistTemplateAsync(ChecklistTemplate throw new InvalidOperationException($"A template named '{template.Name}' already exists."); } - template.OrganizationId = organizationId!; + template.OrganizationId = organizationId!.Value; template.CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; template.CreatedOn = DateTime.UtcNow; @@ -103,7 +103,7 @@ public async Task UpdateChecklistTemplateAsync(ChecklistTemplate template) await _dbContext.SaveChangesAsync(); } - public async Task DeleteChecklistTemplateAsync(int templateId) + public async Task DeleteChecklistTemplateAsync(Guid templateId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -135,7 +135,8 @@ public async Task AddChecklistTemplateItemAsync(Checklist var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - item.OrganizationId = organizationId!; + item.Id = Guid.NewGuid(); + item.OrganizationId = organizationId!.Value; item.CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; item.CreatedOn = DateTime.UtcNow; @@ -160,7 +161,7 @@ public async Task UpdateChecklistTemplateItemAsync(ChecklistTemplateItem item) await _dbContext.SaveChangesAsync(); } - public async Task DeleteChecklistTemplateItemAsync(int itemId) + public async Task DeleteChecklistTemplateItemAsync(Guid itemId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -211,7 +212,7 @@ public async Task> GetChecklistsAsync(bool includeArchived = fal return await query.OrderByDescending(c => c.CreatedOn).ToListAsync(); } - public async Task> GetChecklistsByPropertyIdAsync(int propertyId) + public async Task> GetChecklistsByPropertyIdAsync(Guid propertyId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -231,7 +232,7 @@ public async Task> GetChecklistsByPropertyIdAsync(int propertyId .ToListAsync(); } - public async Task> GetChecklistsByLeaseIdAsync(int leaseId) + public async Task> GetChecklistsByLeaseIdAsync(Guid leaseId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -251,7 +252,7 @@ public async Task> GetChecklistsByLeaseIdAsync(int leaseId) .ToListAsync(); } - public async Task GetChecklistByIdAsync(int checklistId) + public async Task GetChecklistByIdAsync(Guid checklistId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -274,7 +275,7 @@ public async Task> GetChecklistsByLeaseIdAsync(int leaseId) /// /// Creates a new checklist instance from a template, including all template items /// - public async Task CreateChecklistFromTemplateAsync(int templateId) + public async Task CreateChecklistFromTemplateAsync(Guid templateId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -294,11 +295,12 @@ public async Task CreateChecklistFromTemplateAsync(int templateId) // Create the checklist from template var checklist = new Checklist { + Id = Guid.NewGuid(), Name = template.Name, ChecklistType = template.Category, ChecklistTemplateId = template.Id, Status = ApplicationConstants.ChecklistStatuses.Draft, - OrganizationId = organizationId!, + OrganizationId = organizationId!.Value, CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, CreatedOn = DateTime.UtcNow }; @@ -311,6 +313,7 @@ public async Task CreateChecklistFromTemplateAsync(int templateId) { var checklistItem = new ChecklistItem { + Id = Guid.NewGuid(), ChecklistId = checklist.Id, ItemText = templateItem.ItemText, ItemOrder = templateItem.ItemOrder, @@ -318,15 +321,23 @@ public async Task CreateChecklistFromTemplateAsync(int templateId) SectionOrder = templateItem.SectionOrder, RequiresValue = templateItem.RequiresValue, IsChecked = false, - OrganizationId = organizationId! + OrganizationId = organizationId!.Value, + CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, + CreatedOn = DateTime.UtcNow }; _dbContext.ChecklistItems.Add(checklistItem); } await _dbContext.SaveChangesAsync(); - // Reload with items - return await GetChecklistByIdAsync(checklist.Id) ?? checklist; + // Return checklist with items already loaded in memory + checklist.Items = await _dbContext.ChecklistItems + .Where(i => i.ChecklistId == checklist.Id) + .OrderBy(i => i.SectionOrder) + .ThenBy(i => i.ItemOrder) + .ToListAsync(); + + return checklist; } public async Task AddChecklistAsync(Checklist checklist) @@ -339,7 +350,8 @@ public async Task AddChecklistAsync(Checklist checklist) var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - checklist.OrganizationId = organizationId!; + checklist.Id = Guid.NewGuid(); + checklist.OrganizationId = organizationId!.Value; checklist.CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; checklist.CreatedOn = DateTime.UtcNow; @@ -364,7 +376,7 @@ public async Task UpdateChecklistAsync(Checklist checklist) await _dbContext.SaveChangesAsync(); } - public async Task DeleteChecklistAsync(int checklistId) + public async Task DeleteChecklistAsync(Guid checklistId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -393,7 +405,7 @@ public async Task DeleteChecklistAsync(int checklistId) } } - public async Task ArchiveChecklistAsync(int checklistId) + public async Task ArchiveChecklistAsync(Guid checklistId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -415,7 +427,7 @@ public async Task ArchiveChecklistAsync(int checklistId) } } - public async Task UnarchiveChecklistAsync(int checklistId) + public async Task UnarchiveChecklistAsync(Guid checklistId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -437,7 +449,7 @@ public async Task UnarchiveChecklistAsync(int checklistId) } } - public async Task CompleteChecklistAsync(int checklistId) + public async Task CompleteChecklistAsync(Guid checklistId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -506,7 +518,7 @@ public async Task CompleteChecklistAsync(int checklistId) } } - public async Task SaveChecklistAsTemplateAsync(int checklistId, string templateName, string? templateDescription = null) + public async Task SaveChecklistAsTemplateAsync(Guid checklistId, string templateName, string? templateDescription = null) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) @@ -544,7 +556,7 @@ public async Task SaveChecklistAsTemplateAsync(int checklistI Description = templateDescription ?? $"Template created from checklist: {checklist.Name}", Category = checklist.ChecklistType, IsSystemTemplate = false, - OrganizationId = organizationId!, + OrganizationId = organizationId!.Value, CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, CreatedOn = DateTime.UtcNow }; @@ -557,6 +569,7 @@ public async Task SaveChecklistAsTemplateAsync(int checklistI { var templateItem = new ChecklistTemplateItem { + Id = Guid.NewGuid(), ChecklistTemplateId = template.Id, ItemText = item.ItemText, ItemOrder = item.ItemOrder, @@ -565,7 +578,7 @@ public async Task SaveChecklistAsTemplateAsync(int checklistI IsRequired = false, // User can customize this later RequiresValue = item.RequiresValue, AllowsNotes = true, - OrganizationId = organizationId!, + OrganizationId = organizationId!.Value, CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, CreatedOn = DateTime.UtcNow }; @@ -592,7 +605,8 @@ public async Task AddChecklistItemAsync(ChecklistItem item) var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - item.OrganizationId = organizationId!; + item.Id = Guid.NewGuid(); + item.OrganizationId = organizationId!.Value; item.CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; item.CreatedOn = DateTime.UtcNow; @@ -617,7 +631,7 @@ public async Task UpdateChecklistItemAsync(ChecklistItem item) await _dbContext.SaveChangesAsync(); } - public async Task DeleteChecklistItemAsync(int itemId) + public async Task DeleteChecklistItemAsync(Guid itemId) { var userId = await _userContext.GetUserIdAsync(); if (userId == null) diff --git a/Aquiis.SimpleStart/Application/Services/FinancialReportService.cs b/Aquiis.SimpleStart/Application/Services/FinancialReportService.cs index 9a7e7d8..2d8d038 100644 --- a/Aquiis.SimpleStart/Application/Services/FinancialReportService.cs +++ b/Aquiis.SimpleStart/Application/Services/FinancialReportService.cs @@ -17,10 +17,10 @@ public FinancialReportService(IDbContextFactory contextFac /// Generate income statement for a specific period and optional property /// public async Task GenerateIncomeStatementAsync( - string organizationId, + Guid organizationId, DateTime startDate, DateTime endDate, - int? propertyId = null) + Guid? propertyId = null) { using var context = await _contextFactory.CreateDbContextAsync(); @@ -97,7 +97,7 @@ public async Task GenerateIncomeStatementAsync( /// /// Generate rent roll report showing all properties and tenants /// - public async Task> GenerateRentRollAsync(string organizationId, DateTime asOfDate) + public async Task> GenerateRentRollAsync(Guid organizationId, DateTime asOfDate) { using var context = await _contextFactory.CreateDbContextAsync(); @@ -136,7 +136,7 @@ public async Task> GenerateRentRollAsync(string organizationI /// Generate property performance comparison report /// public async Task> GeneratePropertyPerformanceAsync( - string organizationId, + Guid organizationId, DateTime startDate, DateTime endDate) { @@ -213,7 +213,7 @@ public async Task> GeneratePropertyPerformanceAsync( /// /// Generate tax report data for Schedule E /// - public async Task> GenerateTaxReportAsync(string organizationId, int year, int? propertyId = null) + public async Task> GenerateTaxReportAsync(Guid organizationId, int year, Guid? propertyId = null) { using var context = await _contextFactory.CreateDbContextAsync(); var startDate = new DateTime(year, 1, 1); diff --git a/Aquiis.SimpleStart/Application/Services/NoteService.cs b/Aquiis.SimpleStart/Application/Services/NoteService.cs index 0c9820b..7bb68e6 100644 --- a/Aquiis.SimpleStart/Application/Services/NoteService.cs +++ b/Aquiis.SimpleStart/Application/Services/NoteService.cs @@ -20,24 +20,26 @@ public NoteService(ApplicationDbContext context, UserContextService userContext) /// /// Add a note to an entity /// - public async Task AddNoteAsync(string entityType, int entityId, string content) + public async Task AddNoteAsync(string entityType, Guid entityId, string content) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync(); var userFullName = await _userContext.GetUserNameAsync(); + var userEmail = await _userContext.GetUserEmailAsync(); - if (string.IsNullOrEmpty(organizationId) || string.IsNullOrEmpty(userId)) + if (!organizationId.HasValue || string.IsNullOrEmpty(userId)) { throw new InvalidOperationException("User context is not available."); } var note = new Note { - OrganizationId = organizationId!, + Id = Guid.NewGuid(), + OrganizationId = organizationId!.Value, EntityType = entityType, EntityId = entityId, Content = content.Trim(), - UserFullName = !string.IsNullOrWhiteSpace(userFullName) ? userFullName : "Unknown User", + UserFullName = !string.IsNullOrWhiteSpace(userFullName) ? userFullName : userEmail, CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, CreatedOn = DateTime.UtcNow }; @@ -51,7 +53,7 @@ public async Task AddNoteAsync(string entityType, int entityId, string con /// /// Get all notes for an entity, ordered by newest first /// - public async Task> GetNotesAsync(string entityType, int entityId) + public async Task> GetNotesAsync(string entityType, Guid entityId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _context.Notes @@ -67,7 +69,7 @@ public async Task> GetNotesAsync(string entityType, int entityId) /// /// Delete a note (soft delete) /// - public async Task DeleteNoteAsync(int noteId) + public async Task DeleteNoteAsync(Guid noteId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var note = await _context.Notes @@ -90,7 +92,7 @@ public async Task DeleteNoteAsync(int noteId) /// /// Get note count for an entity /// - public async Task GetNoteCountAsync(string entityType, int entityId) + public async Task GetNoteCountAsync(string entityType, Guid entityId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _context.Notes diff --git a/Aquiis.SimpleStart/Application/Services/OrganizationService.cs b/Aquiis.SimpleStart/Application/Services/OrganizationService.cs index aabe521..a227802 100644 --- a/Aquiis.SimpleStart/Application/Services/OrganizationService.cs +++ b/Aquiis.SimpleStart/Application/Services/OrganizationService.cs @@ -27,7 +27,7 @@ public async Task CreateOrganizationAsync(string ownerId, string n { var organization = new Organization { - Id = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), OwnerId = ownerId, Name = name, DisplayName = displayName ?? name, @@ -42,7 +42,7 @@ public async Task CreateOrganizationAsync(string ownerId, string n // Create Owner entry in UserOrganizations var userOrganization = new UserOrganization { - Id = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), UserId = ownerId, OrganizationId = organization.Id, Role = ApplicationConstants.OrganizationRoles.Owner, @@ -54,11 +54,11 @@ public async Task CreateOrganizationAsync(string ownerId, string n }; _dbContext.UserOrganizations.Add(userOrganization); - await _dbContext.SaveChangesAsync(); // add organization settings record with defaults var settings = new OrganizationSettings { + Id = Guid.NewGuid(), OrganizationId = organization.Id, Name = organization.Name, LateFeeEnabled = true, @@ -90,7 +90,7 @@ public async Task CreateOrganizationAsync(Organization organizatio throw new InvalidOperationException("Cannot create organization: User ID is not available in context."); - organization.Id = Guid.NewGuid().ToString(); + organization.Id = Guid.NewGuid(); organization.OwnerId = userId; organization.IsActive = true; organization.CreatedOn = DateTime.UtcNow; @@ -101,7 +101,7 @@ public async Task CreateOrganizationAsync(Organization organizatio // Create Owner entry in UserOrganizations var userOrganization = new UserOrganization { - Id = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), UserId = userId, OrganizationId = organization.Id, Role = ApplicationConstants.OrganizationRoles.Owner, @@ -141,7 +141,7 @@ public async Task CreateOrganizationAsync(Organization organizatio /// /// Get organization by ID /// - public async Task GetOrganizationByIdAsync(string organizationId) + public async Task GetOrganizationByIdAsync(Guid organizationId) { return await _dbContext.Organizations .Include(o => o.UserOrganizations) @@ -195,7 +195,7 @@ public async Task UpdateOrganizationAsync(Organization organization) /// /// Delete organization (soft delete) /// - public async Task DeleteOrganizationAsync(string organizationId, string deletedBy) + public async Task DeleteOrganizationAsync(Guid organizationId, string deletedBy) { var organization = await _dbContext.Organizations.FindAsync(organizationId); if (organization == null || organization.IsDeleted) @@ -231,7 +231,7 @@ public async Task DeleteOrganizationAsync(string organizationId, string de /// /// Check if user is the owner of an organization /// - public async Task IsOwnerAsync(string userId, string organizationId) + public async Task IsOwnerAsync(string userId, Guid organizationId) { var organization = await _dbContext.Organizations.FindAsync(organizationId); return organization != null && organization.OwnerId == userId && !organization.IsDeleted; @@ -240,7 +240,7 @@ public async Task IsOwnerAsync(string userId, string organizationId) /// /// Check if user has administrator role in an organization /// - public async Task IsAdministratorAsync(string userId, string organizationId) + public async Task IsAdministratorAsync(string userId, Guid organizationId) { var role = await GetUserRoleForOrganizationAsync(userId, organizationId); return role == ApplicationConstants.OrganizationRoles.Administrator; @@ -249,7 +249,7 @@ public async Task IsAdministratorAsync(string userId, string organizationI /// /// Check if user can access an organization (has any active role) /// - public async Task CanAccessOrganizationAsync(string userId, string organizationId) + public async Task CanAccessOrganizationAsync(string userId, Guid organizationId) { return await _dbContext.UserOrganizations .AnyAsync(uo => uo.UserId == userId @@ -261,7 +261,7 @@ public async Task CanAccessOrganizationAsync(string userId, string organiz /// /// Get user's role for a specific organization /// - public async Task GetUserRoleForOrganizationAsync(string userId, string organizationId) + public async Task GetUserRoleForOrganizationAsync(string userId, Guid organizationId) { var userOrg = await _dbContext.UserOrganizations .FirstOrDefaultAsync(uo => uo.UserId == userId @@ -279,7 +279,7 @@ public async Task CanAccessOrganizationAsync(string userId, string organiz /// /// Grant a user access to an organization with a specific role /// - public async Task GrantOrganizationAccessAsync(string userId, string organizationId, string role, string grantedBy) + public async Task GrantOrganizationAccessAsync(string userId, Guid organizationId, string role, string grantedBy) { // Validate role if (!ApplicationConstants.OrganizationRoles.IsValid(role)) @@ -317,7 +317,7 @@ public async Task GrantOrganizationAccessAsync(string userId, string organ // Create new access var userOrganization = new UserOrganization { - Id = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), UserId = userId, OrganizationId = organizationId, Role = role, @@ -338,7 +338,7 @@ public async Task GrantOrganizationAccessAsync(string userId, string organ /// /// Revoke a user's access to an organization /// - public async Task RevokeOrganizationAccessAsync(string userId, string organizationId, string revokedBy) + public async Task RevokeOrganizationAccessAsync(string userId, Guid organizationId, string revokedBy) { var userOrg = await _dbContext.UserOrganizations .FirstOrDefaultAsync(uo => uo.UserId == userId @@ -368,7 +368,7 @@ public async Task RevokeOrganizationAccessAsync(string userId, string orga /// /// Update a user's role in an organization /// - public async Task UpdateUserRoleAsync(string userId, string organizationId, string newRole, string modifiedBy) + public async Task UpdateUserRoleAsync(string userId, Guid organizationId, string newRole, string modifiedBy) { // Validate role if (!ApplicationConstants.OrganizationRoles.IsValid(newRole)) @@ -402,7 +402,7 @@ public async Task UpdateUserRoleAsync(string userId, string organizationId /// /// Get all users with access to an organization /// - public async Task> GetOrganizationUsersAsync(string organizationId) + public async Task> GetOrganizationUsersAsync(Guid organizationId) { return await _dbContext.UserOrganizations .Where(uo => uo.OrganizationId == organizationId && uo.IsActive && !uo.IsDeleted && uo.UserId != ApplicationConstants.SystemUser.Id) diff --git a/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs b/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs index af509aa..b5f7bf4 100644 --- a/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs +++ b/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs @@ -58,7 +58,7 @@ public async Task> GetPropertiesAsync() .ToListAsync(); } - public async Task GetPropertyByIdAsync(int propertyId) + public async Task GetPropertyByIdAsync(Guid propertyId) { var _userId = await _userContext.GetUserIdAsync(); @@ -122,7 +122,8 @@ public async Task AddPropertyAsync(Property property) var organizationId = await _userContext.GetActiveOrganizationIdAsync(); // Set tracking fields automatically - property.OrganizationId = organizationId!; + property.Id = Guid.NewGuid(); + property.OrganizationId = organizationId!.Value; property.CreatedBy = _userId; property.CreatedOn = DateTime.UtcNow; @@ -160,13 +161,13 @@ public async Task UpdatePropertyAsync(Property property) // Set tracking fields automatically property.LastModifiedBy = _userId; property.LastModifiedOn = DateTime.UtcNow; - property.OrganizationId = organizationId!; // Prevent org hijacking + property.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(property); await _dbContext.SaveChangesAsync(); } - public async Task DeletePropertyAsync(int propertyId) + public async Task DeletePropertyAsync(Guid propertyId) { var _userId = await _userContext.GetUserIdAsync(); @@ -197,7 +198,7 @@ public async Task DeletePropertyAsync(int propertyId) } } - private async Task SoftDeletePropertyAsync(int propertyId) + private async Task SoftDeletePropertyAsync(Guid propertyId) { var _userId = await _userContext.GetUserIdAsync(); @@ -269,7 +270,7 @@ public async Task> GetTenantsAsync() .ToListAsync(); } - public async Task> GetTenantsByLeaseIdAsync(int leaseId) + public async Task> GetTenantsByLeaseIdAsync(Guid leaseId) { var _userId = await _userContext.GetUserIdAsync(); @@ -292,7 +293,7 @@ public async Task> GetTenantsByLeaseIdAsync(int leaseId) .Where(t => tenantIds.Contains(t.Id) && t.OrganizationId == organizationId && !t.IsDeleted) .ToListAsync(); } - public async Task> GetTenantsByPropertyIdAsync(int propertyId) + public async Task> GetTenantsByPropertyIdAsync(Guid propertyId) { var _userId = await _userContext.GetUserIdAsync(); @@ -316,7 +317,7 @@ public async Task> GetTenantsByPropertyIdAsync(int propertyId) .ToListAsync(); } - public async Task GetTenantByIdAsync(int tenantId) + public async Task GetTenantByIdAsync(Guid tenantId) { var _userId = await _userContext.GetUserIdAsync(); @@ -363,7 +364,8 @@ public async Task AddTenantAsync(Tenant tenant) var organizationId = await _userContext.GetActiveOrganizationIdAsync(); // Set tracking fields automatically - tenant.OrganizationId = organizationId!; + tenant.Id = Guid.NewGuid(); + tenant.OrganizationId = organizationId!.Value; tenant.CreatedBy = _userId; tenant.CreatedOn = DateTime.UtcNow; @@ -397,7 +399,7 @@ public async Task UpdateTenantAsync(Tenant tenant) // Set tracking fields automatically tenant.LastModifiedOn = DateTime.UtcNow; tenant.LastModifiedBy = _userId; - tenant.OrganizationId = organizationId!; // Prevent org hijacking + tenant.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(tenant); await _dbContext.SaveChangesAsync(); @@ -463,7 +465,7 @@ public async Task> GetLeasesAsync() .Where(l => !l.IsDeleted && !l.Tenant!.IsDeleted && !l.Property.IsDeleted && l.Property.OrganizationId == organizationId) .ToListAsync(); } - public async Task GetLeaseByIdAsync(int leaseId) + public async Task GetLeaseByIdAsync(Guid leaseId) { var _userId = await _userContext.GetUserIdAsync(); @@ -481,7 +483,7 @@ public async Task> GetLeasesAsync() .FirstOrDefaultAsync(l => l.Id == leaseId && !l.IsDeleted && (l.Tenant == null || !l.Tenant.IsDeleted) && !l.Property.IsDeleted && l.Property.OrganizationId == organizationId); } - public async Task> GetLeasesByPropertyIdAsync(int propertyId) + public async Task> GetLeasesByPropertyIdAsync(Guid propertyId) { var _userId = await _userContext.GetUserIdAsync(); @@ -504,7 +506,29 @@ public async Task> GetLeasesByPropertyIdAsync(int propertyId) .ToList(); } - public async Task> GetActiveLeasesByPropertyIdAsync(int propertyId) + public async Task> GetCurrentAndUpcomingLeasesByPropertyIdAsync(Guid propertyId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.PropertyId == propertyId + && !l.IsDeleted + && l.Property.OrganizationId == organizationId + && (l.Status == ApplicationConstants.LeaseStatuses.Pending + || l.Status == ApplicationConstants.LeaseStatuses.Active)) + .ToListAsync(); + } + + public async Task> GetActiveLeasesByPropertyIdAsync(Guid propertyId) { var _userId = await _userContext.GetUserIdAsync(); @@ -528,7 +552,7 @@ public async Task> GetActiveLeasesByPropertyIdAsync(int propertyId) } - public async Task> GetLeasesByTenantIdAsync(int tenantId) + public async Task> GetLeasesByTenantIdAsync(Guid tenantId) { var _userId = await _userContext.GetUserIdAsync(); @@ -564,6 +588,8 @@ public async Task> GetLeasesByTenantIdAsync(int tenantId) return lease; // Set tracking fields automatically + lease.Id = Guid.NewGuid(); + lease.OrganizationId = organizationId!.Value; lease.CreatedBy = _userId; lease.CreatedOn = DateTime.UtcNow; @@ -610,7 +636,7 @@ public async Task UpdateLeaseAsync(Lease lease) await _dbContext.SaveChangesAsync(); } - public async Task DeleteLeaseAsync(int leaseId) + public async Task DeleteLeaseAsync(Guid leaseId) { var _userId = await _userContext.GetUserIdAsync(); @@ -643,7 +669,7 @@ public async Task DeleteLeaseAsync(int leaseId) } } - private async Task SoftDeleteLeaseAsync(int leaseId) + private async Task SoftDeleteLeaseAsync(Guid leaseId) { var userId = await _userContext.GetUserIdAsync(); var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -685,7 +711,7 @@ public async Task> GetInvoicesAsync() .ToListAsync(); } - public async Task GetInvoiceByIdAsync(int invoiceId) + public async Task GetInvoiceByIdAsync(Guid invoiceId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -701,7 +727,7 @@ public async Task> GetInvoicesAsync() && i.Lease.Property.OrganizationId == organizationId); } - public async Task> GetInvoicesByLeaseIdAsync(int leaseId) + public async Task> GetInvoicesByLeaseIdAsync(Guid leaseId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -739,7 +765,8 @@ public async Task AddInvoiceAsync(Invoice invoice) } // Set tracking fields automatically - invoice.OrganizationId = organizationId!; + invoice.Id = Guid.NewGuid(); + invoice.OrganizationId = organizationId!.Value; invoice.CreatedBy = _userId; invoice.CreatedOn = DateTime.UtcNow; @@ -771,7 +798,7 @@ public async Task UpdateInvoiceAsync(Invoice invoice) // Set tracking fields automatically invoice.LastModifiedOn = DateTime.UtcNow; invoice.LastModifiedBy = userId; - invoice.OrganizationId = organizationId!; // Prevent org hijacking + invoice.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(invoice); await _dbContext.SaveChangesAsync(); @@ -802,11 +829,11 @@ public async Task DeleteInvoiceAsync(Invoice invoice) public async Task GenerateInvoiceNumberAsync() { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - var lastInvoice = await _dbContext.Invoices.Where(i => i.OrganizationId == organizationId) - .OrderByDescending(i => i.Id) - .FirstOrDefaultAsync(); + var invoiceCount = await _dbContext.Invoices + .Where(i => i.OrganizationId == organizationId) + .CountAsync(); - var nextNumber = lastInvoice != null ? lastInvoice.Id + 1 : 1; + var nextNumber = invoiceCount + 1; return $"INV-{DateTime.Now:yyyyMM}-{nextNumber:D5}"; } @@ -831,7 +858,7 @@ public async Task> GetPaymentsAsync() } - public async Task GetPaymentByIdAsync(int paymentId) + public async Task GetPaymentByIdAsync(Guid paymentId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.Payments @@ -844,7 +871,7 @@ public async Task> GetPaymentsAsync() .FirstOrDefaultAsync(p => p.Id == paymentId && !p.IsDeleted && p.Invoice.Lease.Property.OrganizationId == organizationId); } - public async Task> GetPaymentsByInvoiceIdAsync(int invoiceId) + public async Task> GetPaymentsByInvoiceIdAsync(Guid invoiceId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.Payments @@ -866,7 +893,8 @@ public async Task AddPaymentAsync(Payment payment) var organizationId = await _userContext.GetActiveOrganizationIdAsync(); // Set tracking fields automatically - payment.OrganizationId = organizationId!; + payment.Id = Guid.NewGuid(); + payment.OrganizationId = organizationId!.Value; payment.CreatedBy = _userId; payment.CreatedOn = DateTime.UtcNow; @@ -898,7 +926,7 @@ public async Task UpdatePaymentAsync(Payment payment) } // Set tracking fields automatically - payment.OrganizationId = organizationId!; + payment.OrganizationId = organizationId!.Value; payment.LastModifiedOn = DateTime.UtcNow; payment.LastModifiedBy = _userId; @@ -938,7 +966,7 @@ public async Task DeletePaymentAsync(Payment payment) await UpdateInvoicePaidAmountAsync(invoiceId); } - private async Task UpdateInvoicePaidAmountAsync(int invoiceId) + private async Task UpdateInvoicePaidAmountAsync(Guid invoiceId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var invoice = await _dbContext.Invoices.Where(i => i.Id == invoiceId && i.OrganizationId == organizationId).FirstOrDefaultAsync(); @@ -987,7 +1015,7 @@ public async Task> GetDocumentsAsync() .ToListAsync(); } - public async Task GetDocumentByIdAsync(int documentId) + public async Task GetDocumentByIdAsync(Guid documentId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1004,7 +1032,7 @@ public async Task> GetDocumentsAsync() .FirstOrDefaultAsync(d => d.Id == documentId && !d.IsDeleted && d.OrganizationId == organizationId); } - public async Task> GetDocumentsByLeaseIdAsync(int leaseId) + public async Task> GetDocumentsByLeaseIdAsync(Guid leaseId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1018,7 +1046,7 @@ public async Task> GetDocumentsByLeaseIdAsync(int leaseId) .ToListAsync(); } - public async Task> GetDocumentsByPropertyIdAsync(int propertyId) + public async Task> GetDocumentsByPropertyIdAsync(Guid propertyId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1031,7 +1059,7 @@ public async Task> GetDocumentsByPropertyIdAsync(int propertyId) .ToListAsync(); } - public async Task> GetDocumentsByTenantIdAsync(int tenantId) + public async Task> GetDocumentsByTenantIdAsync(Guid tenantId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1054,8 +1082,9 @@ public async Task AddDocumentAsync(Document document) throw new UnauthorizedAccessException("User is not authenticated."); } + document.Id = Guid.NewGuid(); + document.OrganizationId = organizationId!.Value; document.CreatedBy = _userId; - document.OrganizationId = organizationId!; document.CreatedOn = DateTime.UtcNow; _dbContext.Documents.Add(document); await _dbContext.SaveChangesAsync(); @@ -1084,7 +1113,7 @@ public async Task UpdateDocumentAsync(Document document) // Set tracking fields automatically document.LastModifiedBy = _userId; document.LastModifiedOn = DateTime.UtcNow; - document.OrganizationId = organizationId!; // Prevent org hijacking + document.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(document); await _dbContext.SaveChangesAsync(); @@ -1185,7 +1214,7 @@ public async Task> GetInspectionsAsync() .ToListAsync(); } - public async Task> GetInspectionsByPropertyIdAsync(int propertyId) + public async Task> GetInspectionsByPropertyIdAsync(Guid propertyId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1198,7 +1227,7 @@ public async Task> GetInspectionsByPropertyIdAsync(int property .ToListAsync(); } - public async Task GetInspectionByIdAsync(int inspectionId) + public async Task GetInspectionByIdAsync(Guid inspectionId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1219,7 +1248,9 @@ public async Task AddInspectionAsync(Inspection inspection) { throw new UnauthorizedAccessException("User is not authenticated."); } - inspection.OrganizationId = organizationId!; + + inspection.Id = Guid.NewGuid(); + inspection.OrganizationId = organizationId!.Value; inspection.CreatedBy = _userId; inspection.CreatedOn = DateTime.UtcNow; await _dbContext.Inspections.AddAsync(inspection); @@ -1274,7 +1305,7 @@ public async Task UpdateInspectionAsync(Inspection inspection) // Set tracking fields automatically inspection.LastModifiedBy = _userId; inspection.LastModifiedOn = DateTime.UtcNow; - inspection.OrganizationId = organizationId!; // Prevent org hijacking + inspection.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(inspection); await _dbContext.SaveChangesAsync(); @@ -1283,11 +1314,11 @@ public async Task UpdateInspectionAsync(Inspection inspection) await _calendarEventService.CreateOrUpdateEventAsync(inspection); } - public async Task DeleteInspectionAsync(int inspectionId) + public async Task DeleteInspectionAsync(Guid inspectionId) { var userId = await _userContext.GetUserIdAsync(); var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(organizationId)) + if (string.IsNullOrEmpty(userId) || !organizationId.HasValue || organizationId == Guid.Empty) { throw new UnauthorizedAccessException("User is not authenticated."); } @@ -1320,7 +1351,7 @@ public async Task DeleteInspectionAsync(int inspectionId) /// /// Updates property inspection tracking after a routine inspection is completed /// - public async Task UpdatePropertyInspectionTrackingAsync(int propertyId, DateTime inspectionDate, int intervalMonths = 12) + public async Task UpdatePropertyInspectionTrackingAsync(Guid propertyId, DateTime inspectionDate, int intervalMonths = 12) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1393,7 +1424,7 @@ public async Task GetOverdueInspectionCountAsync() /// /// Initializes inspection tracking for a property (sets first inspection due date) /// - public async Task InitializePropertyInspectionTrackingAsync(int propertyId, int intervalMonths = 12) + public async Task InitializePropertyInspectionTrackingAsync(Guid propertyId, int intervalMonths = 12) { var property = await _dbContext.Properties.FindAsync(propertyId); var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1437,6 +1468,7 @@ private async Task CreateRoutineInspectionCalendarEventAsync(Property property) var calendarEvent = new CalendarEvent { + Id = Guid.NewGuid(), Title = $"Routine Inspection - {property.Address}", Description = $"Routine inspection due for property at {property.Address}, {property.City}, {property.State}", StartOn = property.NextRoutineInspectionDueDate.Value, @@ -1474,7 +1506,7 @@ public async Task> GetMaintenanceRequestsAsync() .ToListAsync(); } - public async Task> GetMaintenanceRequestsByPropertyAsync(int propertyId) + public async Task> GetMaintenanceRequestsByPropertyAsync(Guid propertyId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1486,7 +1518,7 @@ public async Task> GetMaintenanceRequestsByPropertyAsyn .ToListAsync(); } - public async Task> GetMaintenanceRequestsByLeaseAsync(int leaseId) + public async Task> GetMaintenanceRequestsByLeaseAsync(Guid leaseId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1565,7 +1597,7 @@ public async Task GetUrgentMaintenanceRequestCountAsync() .CountAsync(); } - public async Task GetMaintenanceRequestByIdAsync(int id) + public async Task GetMaintenanceRequestByIdAsync(Guid id) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -1585,7 +1617,9 @@ public async Task AddMaintenanceRequestAsync(MaintenanceRequest maintenanceReque } var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - maintenanceRequest.OrganizationId = organizationId!; + // Set tracking fields automatically + maintenanceRequest.Id = Guid.NewGuid(); + maintenanceRequest.OrganizationId = organizationId!.Value; maintenanceRequest.CreatedBy = _userId; maintenanceRequest.CreatedOn = DateTime.UtcNow; @@ -1619,7 +1653,7 @@ public async Task UpdateMaintenanceRequestAsync(MaintenanceRequest maintenanceRe // Set tracking fields automatically maintenanceRequest.LastModifiedBy = _userId; maintenanceRequest.LastModifiedOn = DateTime.UtcNow; - maintenanceRequest.OrganizationId = organizationId!; // Prevent org hijacking + maintenanceRequest.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(maintenanceRequest); await _dbContext.SaveChangesAsync(); @@ -1628,7 +1662,7 @@ public async Task UpdateMaintenanceRequestAsync(MaintenanceRequest maintenanceRe await _calendarEventService.CreateOrUpdateEventAsync(maintenanceRequest); } - public async Task DeleteMaintenanceRequestAsync(int id) + public async Task DeleteMaintenanceRequestAsync(Guid id) { var _userId = await _userContext.GetUserIdAsync(); @@ -1656,7 +1690,7 @@ public async Task DeleteMaintenanceRequestAsync(int id) } } - public async Task UpdateMaintenanceRequestStatusAsync(int id, string status) + public async Task UpdateMaintenanceRequestStatusAsync(Guid id, string status) { var _userId = await _userContext.GetUserIdAsync(); @@ -1698,7 +1732,7 @@ public async Task UpdateMaintenanceRequestStatusAsync(int id, string status) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - if (string.IsNullOrEmpty(organizationId)) + if (!organizationId.HasValue || organizationId == Guid.Empty) { throw new InvalidOperationException("Organization ID not found for current user"); } @@ -1713,7 +1747,7 @@ public async Task UpdateMaintenanceRequestStatusAsync(int id, string status) var userId = await _userContext.GetUserIdAsync(); settings = new OrganizationSettings { - OrganizationId = organizationId, // This should be set to the actual organization ID + OrganizationId = organizationId.Value, // This should be set to the actual organization ID LateFeeEnabled = true, LateFeeAutoApply = true, LateFeeGracePeriodDays = 3, @@ -1732,7 +1766,7 @@ public async Task UpdateMaintenanceRequestStatusAsync(int id, string status) return settings; } - public async Task GetOrganizationSettingsByOrgIdAsync(string organizationId) + public async Task GetOrganizationSettingsByOrgIdAsync(Guid organizationId) { var settings = await _dbContext.OrganizationSettings .Where(s => !s.IsDeleted && s.OrganizationId == organizationId) @@ -1747,11 +1781,11 @@ public async Task UpdateMaintenanceRequestStatusAsync(int id, string status) public async Task UpdateOrganizationSettingsAsync(OrganizationSettings settings) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - if (string.IsNullOrEmpty(organizationId)) + if (!organizationId.HasValue || organizationId == Guid.Empty) { throw new InvalidOperationException("Organization ID not found for current user"); } - if (settings.OrganizationId != organizationId) + if (settings.OrganizationId != organizationId.Value) { throw new InvalidOperationException("Cannot update settings for a different organization"); } @@ -1781,7 +1815,7 @@ public async Task> GetAllProspectiveTenantsAsync() .ToListAsync(); } - public async Task GetProspectiveTenantByIdAsync(int id) + public async Task GetProspectiveTenantByIdAsync(Guid id) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.ProspectiveTenants @@ -1797,7 +1831,8 @@ public async Task CreateProspectiveTenantAsync(ProspectiveTen var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync(); - prospectiveTenant.OrganizationId = organizationId!; + prospectiveTenant.Id = Guid.NewGuid(); + prospectiveTenant.OrganizationId = organizationId!.Value; prospectiveTenant.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; prospectiveTenant.CreatedOn = DateTime.UtcNow; prospectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.Lead; @@ -1831,14 +1866,14 @@ public async Task UpdateProspectiveTenantAsync(ProspectiveTen // Set tracking fields automatically prospectiveTenant.LastModifiedOn = DateTime.UtcNow; prospectiveTenant.LastModifiedBy = userId; - prospectiveTenant.OrganizationId = organizationId!; // Prevent org hijacking + prospectiveTenant.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(prospectiveTenant); await _dbContext.SaveChangesAsync(); return prospectiveTenant; } - public async Task DeleteProspectiveTenantAsync(int id) + public async Task DeleteProspectiveTenantAsync(Guid id) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync(); @@ -1875,7 +1910,7 @@ public async Task> GetAllToursAsync() .ToListAsync(); } - public async Task> GetToursByProspectiveIdAsync(int prospectiveTenantId) + public async Task> GetToursByProspectiveIdAsync(Guid prospectiveTenantId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.Tours @@ -1887,7 +1922,7 @@ public async Task> GetToursByProspectiveIdAsync(int prospectiveTenant .ToListAsync(); } - public async Task GetTourByIdAsync(int id) + public async Task GetTourByIdAsync(Guid id) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.Tours @@ -1898,14 +1933,14 @@ public async Task> GetToursByProspectiveIdAsync(int prospectiveTenant .FirstOrDefaultAsync(); } - public async Task CreateTourAsync(Tour tour, int? templateId = null) + public async Task CreateTourAsync(Tour tour, Guid? templateId = null) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync(); - tour.OrganizationId = organizationId!; + tour.Id = Guid.NewGuid(); + tour.OrganizationId = organizationId!.Value; tour.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; - tour.CreatedOn = DateTime.UtcNow; tour.Status = ApplicationConstants.TourStatuses.Scheduled; @@ -1917,7 +1952,7 @@ public async Task CreateTourAsync(Tour tour, int? templateId = null) // Find the specified template, or fall back to default "Property Tour" template ChecklistTemplate? tourTemplate = null; - if (templateId.HasValue && templateId.Value > 0) + if (templateId.HasValue) { // Use the specified template tourTemplate = await _dbContext.ChecklistTemplates @@ -1992,7 +2027,7 @@ public async Task UpdateTourAsync(Tour tour) // Set tracking fields automatically tour.LastModifiedBy = userId; tour.LastModifiedOn = DateTime.UtcNow; - tour.OrganizationId = organizationId!; // Prevent org hijacking + tour.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(tour); await _dbContext.SaveChangesAsync(); @@ -2003,12 +2038,12 @@ public async Task UpdateTourAsync(Tour tour) return tour; } - public async Task DeleteTourAsync(int id) + public async Task DeleteTourAsync(Guid id) { var userId = await _userContext.GetUserIdAsync(); var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - if(string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(organizationId)) + if(string.IsNullOrEmpty(userId) || !organizationId.HasValue || organizationId == Guid.Empty) { throw new UnauthorizedAccessException("User is not authenticated."); } @@ -2035,7 +2070,7 @@ public async Task DeleteTourAsync(int id) await _calendarEventService.DeleteEventAsync(tour.CalendarEventId); } - public async Task CancelTourAsync(int tourId) + public async Task CancelTourAsync(Guid tourId) { var userId = await _userContext.GetUserIdAsync(); var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -2046,7 +2081,7 @@ public async Task CancelTourAsync(int tourId) throw new InvalidOperationException("Tour not found."); } - if(string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(organizationId) || tour.OrganizationId != organizationId) + if(string.IsNullOrEmpty(userId) || !organizationId.HasValue || tour.OrganizationId != organizationId.Value) { throw new UnauthorizedAccessException("User is not authenticated."); } @@ -2083,7 +2118,7 @@ public async Task CancelTourAsync(int tourId) return true; } - public async Task CompleteTourAsync(int tourId, string? feedback = null, string? interestLevel = null) + public async Task CompleteTourAsync(Guid tourId, string? feedback = null, string? interestLevel = null) { var userId = await _userContext.GetUserIdAsync(); var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -2091,7 +2126,7 @@ public async Task CompleteTourAsync(int tourId, string? feedback = null, s var tour = await GetTourByIdAsync(tourId); if (tour == null) return false; - if(string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(organizationId) || tour.OrganizationId != organizationId) + if(string.IsNullOrEmpty(userId) || !organizationId.HasValue || tour.OrganizationId != organizationId.Value) { throw new UnauthorizedAccessException("User is not authenticated."); } @@ -2122,7 +2157,7 @@ public async Task CompleteTourAsync(int tourId, string? feedback = null, s return true; } - public async Task MarkTourAsNoShowAsync(int tourId) + public async Task MarkTourAsNoShowAsync(Guid tourId) { var userId = await _userContext.GetUserIdAsync(); var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -2130,7 +2165,7 @@ public async Task MarkTourAsNoShowAsync(int tourId) var tour = await GetTourByIdAsync(tourId); if (tour == null) return false; - if(string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(organizationId) || tour.OrganizationId != organizationId) + if(string.IsNullOrEmpty(userId) || !organizationId.HasValue || tour.OrganizationId != organizationId.Value) { throw new UnauthorizedAccessException("User is not authenticated."); } @@ -2172,7 +2207,7 @@ public async Task> GetAllRentalApplicationsAsync() .ToListAsync(); } - public async Task GetRentalApplicationByIdAsync(int id) + public async Task GetRentalApplicationByIdAsync(Guid id) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.RentalApplications @@ -2183,7 +2218,7 @@ public async Task> GetAllRentalApplicationsAsync() .FirstOrDefaultAsync(); } - public async Task GetApplicationByProspectiveIdAsync(int prospectiveTenantId) + public async Task GetApplicationByProspectiveIdAsync(Guid prospectiveTenantId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.RentalApplications @@ -2198,7 +2233,8 @@ public async Task CreateRentalApplicationAsync(RentalApplicat var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync(); - application.OrganizationId = organizationId!; + application.Id = Guid.NewGuid(); + application.OrganizationId = organizationId!.Value; application.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; application.CreatedOn = DateTime.UtcNow; application.AppliedOn = DateTime.UtcNow; @@ -2283,14 +2319,14 @@ public async Task UpdateRentalApplicationAsync(RentalApplicat // Set tracking fields automatically application.LastModifiedBy = userId; application.LastModifiedOn = DateTime.UtcNow; - application.OrganizationId = organizationId!; // Prevent org hijacking + application.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(application); await _dbContext.SaveChangesAsync(); return application; } - public async Task DeleteRentalApplicationAsync(int id) + public async Task DeleteRentalApplicationAsync(Guid id) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync(); @@ -2316,7 +2352,7 @@ public async Task DeleteRentalApplicationAsync(int id) #region ApplicationScreening CRUD - public async Task GetScreeningByApplicationIdAsync(int rentalApplicationId) + public async Task GetScreeningByApplicationIdAsync(Guid rentalApplicationId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.ApplicationScreenings @@ -2329,8 +2365,9 @@ public async Task CreateScreeningAsync(ApplicationScreenin { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync(); - - screening.OrganizationId = organizationId!; + + screening.Id = Guid.NewGuid(); + screening.OrganizationId = organizationId!.Value; screening.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; screening.CreatedOn = DateTime.UtcNow; screening.OverallResult = ApplicationConstants.ScreeningResults.Pending; @@ -2381,7 +2418,7 @@ public async Task UpdateScreeningAsync(ApplicationScreenin // Set tracking fields automatically screening.LastModifiedOn = DateTime.UtcNow; screening.LastModifiedBy = userId; - screening.OrganizationId = organizationId!; // Prevent org hijacking + screening.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(screening); await _dbContext.SaveChangesAsync(); @@ -2392,7 +2429,7 @@ public async Task UpdateScreeningAsync(ApplicationScreenin #region Business Logic - public async Task ApproveApplicationAsync(int applicationId) + public async Task ApproveApplicationAsync(Guid applicationId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync() ?? string.Empty; @@ -2426,7 +2463,7 @@ public async Task ApproveApplicationAsync(int applicationId) return true; } - public async Task DenyApplicationAsync(int applicationId, string reason) + public async Task DenyApplicationAsync(Guid applicationId, string reason) { var userId = await _userContext.GetUserIdAsync() ?? string.Empty; var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -2456,7 +2493,7 @@ public async Task DenyApplicationAsync(int applicationId, string reason) return true; } - public async Task WithdrawApplicationAsync(int applicationId, string? reason = null) + public async Task WithdrawApplicationAsync(Guid applicationId, string? reason = null) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync() ?? string.Empty; @@ -2564,7 +2601,8 @@ public async Task> GetPendingApplicationsAsync() var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var userId = await _userContext.GetUserIdAsync(); - leaseOffer.OrganizationId = organizationId!; + leaseOffer.Id = Guid.NewGuid(); + leaseOffer.OrganizationId = organizationId!.Value; leaseOffer.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; leaseOffer.CreatedOn = DateTime.UtcNow; _dbContext.LeaseOffers.Add(leaseOffer); @@ -2572,7 +2610,7 @@ public async Task> GetPendingApplicationsAsync() return leaseOffer; } - public async Task GetLeaseOfferByIdAsync(int leaseOfferId) + public async Task GetLeaseOfferByIdAsync(Guid leaseOfferId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.LeaseOffers @@ -2582,7 +2620,7 @@ public async Task> GetPendingApplicationsAsync() .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && lo.OrganizationId == organizationId && !lo.IsDeleted); } - public async Task GetLeaseOfferByApplicationIdAsync(int applicationId) + public async Task GetLeaseOfferByApplicationIdAsync(Guid applicationId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.LeaseOffers @@ -2592,7 +2630,7 @@ public async Task> GetPendingApplicationsAsync() .FirstOrDefaultAsync(lo => lo.RentalApplicationId == applicationId && lo.OrganizationId == organizationId && !lo.IsDeleted); } - public async Task> GetLeaseOffersByPropertyIdAsync(int propertyId) + public async Task> GetLeaseOffersByPropertyIdAsync(Guid propertyId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); return await _dbContext.LeaseOffers @@ -2627,7 +2665,7 @@ public async Task UpdateLeaseOfferAsync(LeaseOffer leaseOffer) // Set tracking fields automatically leaseOffer.LastModifiedBy = userId; leaseOffer.LastModifiedOn = DateTime.UtcNow; - leaseOffer.OrganizationId = organizationId!; // Prevent org hijacking + leaseOffer.OrganizationId = organizationId!.Value; // Prevent org hijacking _dbContext.Entry(existing).CurrentValues.SetValues(leaseOffer); await _dbContext.SaveChangesAsync(); diff --git a/Aquiis.SimpleStart/Application/Services/ScheduledTaskService.cs b/Aquiis.SimpleStart/Application/Services/ScheduledTaskService.cs index d20768b..3333249 100644 --- a/Aquiis.SimpleStart/Application/Services/ScheduledTaskService.cs +++ b/Aquiis.SimpleStart/Application/Services/ScheduledTaskService.cs @@ -125,7 +125,7 @@ private async Task DoWork(CancellationToken stoppingToken) private async Task ApplyLateFees( ApplicationDbContext dbContext, ToastService toastService, - string organizationId, + Guid organizationId, OrganizationSettings settings, CancellationToken stoppingToken) { @@ -176,7 +176,7 @@ private async Task ApplyLateFees( } } - private async Task UpdateInvoiceStatuses(ApplicationDbContext dbContext, string organizationId, CancellationToken stoppingToken) + private async Task UpdateInvoiceStatuses(ApplicationDbContext dbContext, Guid organizationId, CancellationToken stoppingToken) { try { @@ -213,7 +213,7 @@ private async Task UpdateInvoiceStatuses(ApplicationDbContext dbContext, string private async Task SendPaymentReminders( ApplicationDbContext dbContext, - string organizationId, + Guid organizationId, OrganizationSettings settings, CancellationToken stoppingToken) { @@ -265,7 +265,7 @@ private async Task SendPaymentReminders( } } - private async Task CheckLeaseRenewals(ApplicationDbContext dbContext, string organizationId, CancellationToken stoppingToken) + private async Task CheckLeaseRenewals(ApplicationDbContext dbContext, Guid organizationId, CancellationToken stoppingToken) { try { diff --git a/Aquiis.SimpleStart/Application/Services/SecurityDepositService.cs b/Aquiis.SimpleStart/Application/Services/SecurityDepositService.cs index 40429df..151ce6f 100644 --- a/Aquiis.SimpleStart/Application/Services/SecurityDepositService.cs +++ b/Aquiis.SimpleStart/Application/Services/SecurityDepositService.cs @@ -27,15 +27,15 @@ public SecurityDepositService(ApplicationDbContext context, UserContextService u /// Collects a security deposit for a lease. /// public async Task CollectSecurityDepositAsync( - int leaseId, + Guid leaseId, decimal amount, string paymentMethod, string? transactionReference, - int? tenantId = null) + Guid? tenantId = null) { var userId = await _userContext.GetUserIdAsync(); var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - if (string.IsNullOrEmpty(organizationId)) + if (!organizationId.HasValue) throw new InvalidOperationException("Organization context is required"); var lease = await _context.Leases @@ -53,12 +53,12 @@ public async Task CollectSecurityDepositAsync( throw new InvalidOperationException($"Security deposit already exists for lease {leaseId}"); // Use provided tenantId or fall back to lease.TenantId - int depositTenantId; + Guid depositTenantId; if (tenantId.HasValue) { depositTenantId = tenantId.Value; } - else if (lease.TenantId > 0) + else if (lease.TenantId != Guid.Empty) { depositTenantId = lease.TenantId; } @@ -69,7 +69,7 @@ public async Task CollectSecurityDepositAsync( var deposit = new SecurityDeposit { - OrganizationId = organizationId, + OrganizationId = organizationId.Value, LeaseId = leaseId, TenantId = depositTenantId, Amount = amount, @@ -91,7 +91,7 @@ public async Task CollectSecurityDepositAsync( /// /// Adds a security deposit to the investment pool when lease becomes active. /// - public async Task AddToInvestmentPoolAsync(int securityDepositId) + public async Task AddToInvestmentPoolAsync(Guid securityDepositId) { var userId = await _userContext.GetUserIdAsync(); @@ -128,7 +128,7 @@ public async Task AddToInvestmentPoolAsync(int securityDepositId) /// /// Removes a security deposit from the investment pool when lease ends. /// - public async Task RemoveFromInvestmentPoolAsync(int securityDepositId) + public async Task RemoveFromInvestmentPoolAsync(Guid securityDepositId) { var userId = await _userContext.GetUserIdAsync(); @@ -164,7 +164,7 @@ public async Task RemoveFromInvestmentPoolAsync(int securityDepositId) /// /// Gets security deposit by lease ID. /// - public async Task GetSecurityDepositByLeaseIdAsync(int leaseId) + public async Task GetSecurityDepositByLeaseIdAsync(Guid leaseId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -242,7 +242,7 @@ public async Task GetOrCreateInvestmentPoolAsync( { var userId = await _userContext.GetUserIdAsync(); var organizationId = await _userContext.GetActiveOrganizationIdAsync(); - if (string.IsNullOrEmpty(organizationId)) + if (!organizationId.HasValue) throw new InvalidOperationException("Organization context is required"); var pool = await _context.SecurityDepositInvestmentPools @@ -259,7 +259,7 @@ public async Task GetOrCreateInvestmentPoolAsync( pool = new SecurityDepositInvestmentPool { - OrganizationId = organizationId, + OrganizationId = organizationId.Value, Year = year, StartingBalance = 0, EndingBalance = 0, @@ -411,7 +411,7 @@ public async Task> CalculateDividendsAsync(int yea var dividend = new SecurityDepositDividend { - OrganizationId = organizationId, + OrganizationId = organizationId.Value, SecurityDepositId = deposit.Id, InvestmentPoolId = pool.Id, LeaseId = deposit.LeaseId, @@ -457,7 +457,7 @@ public async Task> CalculateDividendsAsync(int yea /// /// Gets an investment pool by ID. /// - public async Task GetInvestmentPoolByIdAsync(int poolId) + public async Task GetInvestmentPoolByIdAsync(Guid poolId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -490,7 +490,7 @@ public async Task> GetInvestmentPoolsAsync() /// Records tenant's payment method choice for dividend. /// public async Task RecordDividendChoiceAsync( - int dividendId, + Guid dividendId, string paymentMethod, string? mailingAddress) { @@ -528,7 +528,7 @@ public async Task RecordDividendChoiceAsync( /// Processes dividend payment (applies as credit or marks as paid). /// public async Task ProcessDividendPaymentAsync( - int dividendId, + Guid dividendId, string? paymentReference) { var userId = await _userContext.GetUserIdAsync(); @@ -566,7 +566,7 @@ public async Task ProcessDividendPaymentAsync( /// /// Gets dividends for a specific tenant. /// - public async Task> GetTenantDividendsAsync(int tenantId) + public async Task> GetTenantDividendsAsync(Guid tenantId) { var organizationId = await _userContext.GetActiveOrganizationIdAsync(); @@ -610,7 +610,7 @@ public async Task> GetDividendsByYearAsync(int yea /// Calculates total refund amount (deposit + dividends - deductions). /// public async Task CalculateRefundAmountAsync( - int securityDepositId, + Guid securityDepositId, decimal deductionsAmount) { var deposit = await _context.SecurityDeposits @@ -632,7 +632,7 @@ public async Task CalculateRefundAmountAsync( /// Processes security deposit refund. /// public async Task ProcessRefundAsync( - int securityDepositId, + Guid securityDepositId, decimal deductionsAmount, string? deductionsReason, string refundMethod, @@ -707,7 +707,7 @@ public async Task> GetPendingRefundsAsync() /// /// Closes an investment pool, marking it as complete. /// - public async Task CloseInvestmentPoolAsync(int poolId) + public async Task CloseInvestmentPoolAsync(Guid poolId) { var userId = await _userContext.GetUserIdAsync(); diff --git a/Aquiis.SimpleStart/Application/Services/TenantConversionService.cs b/Aquiis.SimpleStart/Application/Services/TenantConversionService.cs index f4203f6..052f884 100644 --- a/Aquiis.SimpleStart/Application/Services/TenantConversionService.cs +++ b/Aquiis.SimpleStart/Application/Services/TenantConversionService.cs @@ -31,7 +31,7 @@ public TenantConversionService( /// /// ID of the prospective tenant to convert /// The newly created Tenant, or existing Tenant if already converted - public async Task ConvertProspectToTenantAsync(int prospectiveTenantId) + public async Task ConvertProspectToTenantAsync(Guid prospectiveTenantId) { try { @@ -93,7 +93,7 @@ public TenantConversionService( /// /// Gets tenant by ProspectiveTenantId, or null if not yet converted /// - public async Task GetTenantByProspectIdAsync(int prospectiveTenantId) + public async Task GetTenantByProspectIdAsync(Guid prospectiveTenantId) { return await _context.Tenants .FirstOrDefaultAsync(t => t.ProspectiveTenantId == prospectiveTenantId && !t.IsDeleted); @@ -102,7 +102,7 @@ public TenantConversionService( /// /// Checks if a prospect has already been converted to a tenant /// - public async Task IsProspectAlreadyConvertedAsync(int prospectiveTenantId) + public async Task IsProspectAlreadyConvertedAsync(Guid prospectiveTenantId) { return await _context.Tenants .AnyAsync(t => t.ProspectiveTenantId == prospectiveTenantId && !t.IsDeleted); @@ -111,7 +111,7 @@ public async Task IsProspectAlreadyConvertedAsync(int prospectiveTenantId) /// /// Gets the ProspectiveTenant history for a given Tenant /// - public async Task GetProspectHistoryForTenantAsync(int tenantId) + public async Task GetProspectHistoryForTenantAsync(Guid tenantId) { var tenant = await _context.Tenants .FirstOrDefaultAsync(t => t.Id == tenantId && !t.IsDeleted); diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs index a763330..73cee5c 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs @@ -30,11 +30,15 @@ public enum ApplicationStatus /// public class ApplicationWorkflowService : BaseWorkflowService, IWorkflowState { + private readonly NoteService _noteService; + public ApplicationWorkflowService( ApplicationDbContext context, - UserContextService userContext) + UserContextService userContext, + NoteService noteService) : base(context, userContext) { + _noteService = noteService; } #region State Machine Implementation @@ -99,8 +103,8 @@ public string GetInvalidTransitionReason(ApplicationStatus fromStatus, Applicati /// Creates application, updates property status if first app, and updates prospect status. /// public async Task> SubmitApplicationAsync( - int prospectId, - int propertyId, + Guid prospectId, + Guid propertyId, ApplicationSubmissionModel model) { return await ExecuteWorkflowAsync(async () => @@ -115,14 +119,15 @@ public async Task> SubmitApplicationAsync( // Get organization settings for expiration days var settings = await _context.OrganizationSettings - .FirstOrDefaultAsync(s => s.OrganizationId == orgId.ToString()); + .FirstOrDefaultAsync(s => s.OrganizationId == orgId); var expirationDays = settings?.ApplicationExpirationDays ?? 30; // Create application var application = new RentalApplication { - OrganizationId = orgId.ToString(), + Id = Guid.NewGuid(), + OrganizationId = orgId, ProspectiveTenantId = prospectId, PropertyId = propertyId, Status = ApplicationConstants.ApplicationStatuses.Submitted, @@ -154,11 +159,11 @@ public async Task> SubmitApplicationAsync( }; _context.RentalApplications.Add(application); - await _context.SaveChangesAsync(); // Save to get application ID for logging + // Note: EF Core will assign ID when transaction commits // Update property status if this is first application var property = await _context.Properties - .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == orgId.ToString()); + .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == orgId); if (property != null && property.Status == ApplicationConstants.PropertyStatuses.Available) { @@ -169,7 +174,7 @@ public async Task> SubmitApplicationAsync( // Update prospect status var prospect = await _context.ProspectiveTenants - .FirstOrDefaultAsync(p => p.Id == prospectId && p.OrganizationId == orgId.ToString()); + .FirstOrDefaultAsync(p => p.Id == prospectId && p.OrganizationId == orgId); if (prospect != null) { @@ -205,7 +210,7 @@ await LogTransitionAsync( /// /// Marks an application as under manual review. /// - public async Task MarkApplicationUnderReviewAsync(int applicationId) + public async Task MarkApplicationUnderReviewAsync(Guid applicationId) { return await ExecuteWorkflowAsync(async () => { @@ -248,7 +253,7 @@ await LogTransitionAsync( /// Requires application fee to be paid. /// public async Task> InitiateScreeningAsync( - int applicationId, + Guid applicationId, bool requestBackgroundCheck, bool requestCreditCheck) { @@ -258,10 +263,29 @@ public async Task> InitiateScreeningAsync( if (application == null) return WorkflowResult.Fail("Application not found"); + var userId = await GetCurrentUserIdAsync(); + var orgId = await GetActiveOrganizationIdAsync(); + + // Auto-transition from Submitted to UnderReview if needed + if (application.Status == ApplicationConstants.ApplicationStatuses.Submitted) + { + application.Status = ApplicationConstants.ApplicationStatuses.UnderReview; + application.DecisionBy = userId; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + await LogTransitionAsync( + "RentalApplication", + applicationId, + ApplicationConstants.ApplicationStatuses.Submitted, + ApplicationConstants.ApplicationStatuses.UnderReview, + "AutoTransition-InitiateScreening"); + } + // Validate state if (application.Status != ApplicationConstants.ApplicationStatuses.UnderReview) return WorkflowResult.Fail( - $"Application must be Under Review to initiate screening. Current status: {application.Status}"); + $"Application must be Submitted or Under Review to initiate screening. Current status: {application.Status}"); // Validate application fee paid if (!application.ApplicationFeePaid) @@ -276,13 +300,10 @@ public async Task> InitiateScreeningAsync( return WorkflowResult.Fail( "Screening already exists for this application"); - var userId = await GetCurrentUserIdAsync(); - var orgId = await GetActiveOrganizationIdAsync(); - // Create screening record var screening = new ApplicationScreening { - OrganizationId = orgId.ToString(), + OrganizationId = orgId, RentalApplicationId = applicationId, BackgroundCheckRequested = requestBackgroundCheck, BackgroundCheckRequestedOn = requestBackgroundCheck ? DateTime.UtcNow : null, @@ -327,7 +348,7 @@ await LogTransitionAsync( /// Approves an application after screening review. /// Requires screening to be completed with passing result. /// - public async Task ApproveApplicationAsync(int applicationId) + public async Task ApproveApplicationAsync(Guid applicationId) { return await ExecuteWorkflowAsync(async () => { @@ -383,7 +404,7 @@ await LogTransitionAsync( /// Denies an application with a required reason. /// Rolls back property status if no other pending applications exist. /// - public async Task DenyApplicationAsync(int applicationId, string denialReason) + public async Task DenyApplicationAsync(Guid applicationId, string denialReason) { return await ExecuteWorkflowAsync(async () => { @@ -444,7 +465,7 @@ await LogTransitionAsync( /// Withdraws an application (initiated by prospect). /// Rolls back property status if no other pending applications exist. /// - public async Task WithdrawApplicationAsync(int applicationId, string withdrawalReason) + public async Task WithdrawApplicationAsync(Guid applicationId, string withdrawalReason) { return await ExecuteWorkflowAsync(async () => { @@ -503,11 +524,504 @@ await LogTransitionAsync( }); } + /// + /// Updates screening results after background/credit checks are completed. + /// Does not automatically approve - requires manual ApproveApplicationAsync call. + /// + public async Task CompleteScreeningAsync( + Guid applicationId, + ScreeningResultModel results) + { + return await ExecuteWorkflowAsync(async () => + { + var application = await GetApplicationAsync(applicationId); + if (application == null) + return WorkflowResult.Fail("Application not found"); + + if (application.Status != ApplicationConstants.ApplicationStatuses.Screening) + return WorkflowResult.Fail( + $"Application must be in Screening status. Current status: {application.Status}"); + + if (application.Screening == null) + return WorkflowResult.Fail("Screening record not found"); + + var userId = await GetCurrentUserIdAsync(); + + // Update screening results + var screening = application.Screening; + + if (results.BackgroundCheckPassed.HasValue) + { + screening.BackgroundCheckPassed = results.BackgroundCheckPassed; + screening.BackgroundCheckCompletedOn = DateTime.UtcNow; + screening.BackgroundCheckNotes = results.BackgroundCheckNotes; + } + + if (results.CreditCheckPassed.HasValue) + { + screening.CreditCheckPassed = results.CreditCheckPassed; + screening.CreditScore = results.CreditScore; + screening.CreditCheckCompletedOn = DateTime.UtcNow; + screening.CreditCheckNotes = results.CreditCheckNotes; + } + + screening.OverallResult = results.OverallResult; + screening.ResultNotes = results.ResultNotes; + screening.LastModifiedBy = userId; + screening.LastModifiedOn = DateTime.UtcNow; + + await LogTransitionAsync( + "ApplicationScreening", + screening.Id, + "Pending", + screening.OverallResult, + "CompleteScreening", + results.ResultNotes); + + return WorkflowResult.Ok("Screening results updated successfully"); + + }); + } + + /// + /// Generates a lease offer for an approved application. + /// Creates LeaseOffer entity, updates property to LeasePending, and denies competing applications. + /// + public async Task> GenerateLeaseOfferAsync( + Guid applicationId, + LeaseOfferModel model) + { + return await ExecuteWorkflowAsync(async () => + { + var application = await GetApplicationAsync(applicationId); + if (application == null) + return WorkflowResult.Fail("Application not found"); + + // Validate application approved + if (application.Status != ApplicationConstants.ApplicationStatuses.Approved) + return WorkflowResult.Fail( + $"Application must be Approved to generate lease offer. Current status: {application.Status}"); + + // Validate property not already leased + var property = application.Property; + if (property == null) + return WorkflowResult.Fail("Property not found"); + + if (property.Status == ApplicationConstants.PropertyStatuses.Occupied) + return WorkflowResult.Fail("Property is already occupied"); + + // Validate lease dates + if (model.StartDate >= model.EndDate) + return WorkflowResult.Fail("End date must be after start date"); + + if (model.StartDate < DateTime.Today) + return WorkflowResult.Fail("Start date cannot be in the past"); + + if (model.MonthlyRent <= 0 || model.SecurityDeposit < 0) + return WorkflowResult.Fail("Invalid rent or deposit amount"); + + var userId = await GetCurrentUserIdAsync(); + var orgId = await GetActiveOrganizationIdAsync(); + + // Create lease offer + var leaseOffer = new LeaseOffer + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + RentalApplicationId = applicationId, + PropertyId = property.Id, + ProspectiveTenantId = application.ProspectiveTenantId, + StartDate = model.StartDate, + EndDate = model.EndDate, + MonthlyRent = model.MonthlyRent, + SecurityDeposit = model.SecurityDeposit, + Terms = model.Terms, + Notes = model.Notes ?? string.Empty, + OfferedOn = DateTime.UtcNow, + ExpiresOn = DateTime.UtcNow.AddDays(30), + Status = "Pending", + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.LeaseOffers.Add(leaseOffer); + // Note: EF Core will assign ID when transaction commits + + // Update application + var oldAppStatus = application.Status; + application.Status = ApplicationConstants.ApplicationStatuses.LeaseOffered; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.LeaseOffered; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + + // Update property to LeasePending + property.Status = ApplicationConstants.PropertyStatuses.LeasePending; + property.LastModifiedBy = userId; + property.LastModifiedOn = DateTime.UtcNow; + + // Deny all competing applications + var competingApps = await _context.RentalApplications + .Where(a => a.PropertyId == property.Id && + a.Id != applicationId && + a.OrganizationId == orgId && + (a.Status == ApplicationConstants.ApplicationStatuses.Submitted || + a.Status == ApplicationConstants.ApplicationStatuses.UnderReview || + a.Status == ApplicationConstants.ApplicationStatuses.Screening || + a.Status == ApplicationConstants.ApplicationStatuses.Approved) && + !a.IsDeleted) + .Include(a => a.ProspectiveTenant) + .ToListAsync(); + + foreach (var competingApp in competingApps) + { + competingApp.Status = ApplicationConstants.ApplicationStatuses.Denied; + competingApp.DenialReason = "Property leased to another applicant"; + competingApp.DecidedOn = DateTime.UtcNow; + competingApp.DecisionBy = userId; + competingApp.LastModifiedBy = userId; + competingApp.LastModifiedOn = DateTime.UtcNow; + + if (competingApp.ProspectiveTenant != null) + { + competingApp.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.Denied; + competingApp.ProspectiveTenant.LastModifiedBy = userId; + competingApp.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + + await LogTransitionAsync( + "RentalApplication", + competingApp.Id, + competingApp.Status, + ApplicationConstants.ApplicationStatuses.Denied, + "DenyCompetingApplication", + "Property leased to another applicant"); + } + + await LogTransitionAsync( + "RentalApplication", + applicationId, + oldAppStatus, + application.Status, + "GenerateLeaseOffer"); + + await LogTransitionAsync( + "LeaseOffer", + leaseOffer.Id, + null, + "Pending", + "GenerateLeaseOffer"); + + return WorkflowResult.Ok( + leaseOffer, + $"Lease offer generated successfully. {competingApps.Count} competing application(s) denied."); + + }); + } + + /// + /// Accepts a lease offer and converts prospect to tenant. + /// Creates Tenant and Lease entities, updates property to Occupied. + /// Records security deposit payment. + /// + public async Task> AcceptLeaseOfferAsync( + Guid leaseOfferId, + string depositPaymentMethod, + DateTime depositPaymentDate, + string? depositReferenceNumber = null, + string? depositNotes = null) + { + return await ExecuteWorkflowAsync(async () => + { + var orgId = await GetActiveOrganizationIdAsync(); + var userId = await GetCurrentUserIdAsync(); + + var leaseOffer = await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .ThenInclude(a => a.ProspectiveTenant) + .Include(lo => lo.Property) + .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && + lo.OrganizationId == orgId && + !lo.IsDeleted); + + if (leaseOffer == null) + return WorkflowResult.Fail("Lease offer not found"); + + if (leaseOffer.Status != "Pending") + return WorkflowResult.Fail($"Lease offer status is {leaseOffer.Status}, not Pending"); + + if (leaseOffer.ExpiresOn < DateTime.UtcNow) + return WorkflowResult.Fail("Lease offer has expired"); + + var prospect = leaseOffer.RentalApplication?.ProspectiveTenant; + if (prospect == null) + return WorkflowResult.Fail("Prospective tenant not found"); + + // Convert prospect to tenant + var tenant = new Tenant + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + FirstName = prospect.FirstName, + LastName = prospect.LastName, + Email = prospect.Email, + PhoneNumber = prospect.Phone, + DateOfBirth = prospect.DateOfBirth, + IdentificationNumber = prospect.IdentificationNumber ?? $"ID-{Guid.NewGuid().ToString("N")[..8]}", + ProspectiveTenantId = prospect.Id, + IsActive = true, + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.Tenants.Add(tenant); + // Note: EF Core will assign ID when transaction commits + + // Create lease + var lease = new Lease + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + PropertyId = leaseOffer.PropertyId, + Tenant = tenant, // Use navigation property instead of TenantId + LeaseOfferId = leaseOffer.Id, + StartDate = leaseOffer.StartDate, + EndDate = leaseOffer.EndDate, + MonthlyRent = leaseOffer.MonthlyRent, + SecurityDeposit = leaseOffer.SecurityDeposit, + Terms = leaseOffer.Terms, + Status = ApplicationConstants.LeaseStatuses.Active, + SignedOn = DateTime.UtcNow, + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.Leases.Add(lease); + // Note: EF Core will assign ID when transaction commits + + // Create security deposit record + var securityDeposit = new SecurityDeposit + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + Lease = lease, // Use navigation property + Tenant = tenant, // Use navigation property + Amount = leaseOffer.SecurityDeposit, + DateReceived = depositPaymentDate, + PaymentMethod = depositPaymentMethod, + TransactionReference = depositReferenceNumber, + Status = "Held", + InInvestmentPool = true, + PoolEntryDate = leaseOffer.StartDate, + Notes = depositNotes, + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.SecurityDeposits.Add(securityDeposit); + + // Update lease offer + leaseOffer.Status = "Accepted"; + leaseOffer.RespondedOn = DateTime.UtcNow; + leaseOffer.ConvertedLeaseId = lease.Id; + leaseOffer.LastModifiedBy = userId; + leaseOffer.LastModifiedOn = DateTime.UtcNow; + + // Update application + var application = leaseOffer.RentalApplication; + if (application != null) + { + application.Status = ApplicationConstants.ApplicationStatuses.LeaseAccepted; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + } + + // Update prospect + prospect.Status = ApplicationConstants.ProspectiveStatuses.ConvertedToTenant; + prospect.LastModifiedBy = userId; + prospect.LastModifiedOn = DateTime.UtcNow; + + // Update property + var property = leaseOffer.Property; + if (property != null) + { + property.Status = ApplicationConstants.PropertyStatuses.Occupied; + property.LastModifiedBy = userId; + property.LastModifiedOn = DateTime.UtcNow; + } + + await LogTransitionAsync( + "LeaseOffer", + leaseOfferId, + "Pending", + "Accepted", + "AcceptLeaseOffer"); + + await LogTransitionAsync( + "ProspectiveTenant", + prospect.Id, + ApplicationConstants.ProspectiveStatuses.LeaseOffered, + ApplicationConstants.ProspectiveStatuses.ConvertedToTenant, + "AcceptLeaseOffer"); + + // Add note if lease start date is in the future + if (leaseOffer.StartDate > DateTime.Today) + { + var noteContent = $"Lease accepted on {DateTime.Today:MMM dd, yyyy}. Lease start date: {leaseOffer.StartDate:MMM dd, yyyy}."; + await _noteService.AddNoteAsync(ApplicationConstants.EntityTypes.Lease, lease.Id, noteContent); + } + + return WorkflowResult.Ok(lease, "Lease offer accepted and tenant created successfully"); + + }); + } + + /// + /// Declines a lease offer. + /// Rolls back property status and marks prospect as lease declined. + /// + public async Task DeclineLeaseOfferAsync(Guid leaseOfferId, string declineReason) + { + return await ExecuteWorkflowAsync(async () => + { + if (string.IsNullOrWhiteSpace(declineReason)) + return WorkflowResult.Fail("Decline reason is required"); + + var orgId = await GetActiveOrganizationIdAsync(); + var userId = await GetCurrentUserIdAsync(); + + var leaseOffer = await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .ThenInclude(a => a.ProspectiveTenant) + .Include(lo => lo.Property) + .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && + lo.OrganizationId == orgId && + !lo.IsDeleted); + + if (leaseOffer == null) + return WorkflowResult.Fail("Lease offer not found"); + + if (leaseOffer.Status != "Pending") + return WorkflowResult.Fail($"Lease offer status is {leaseOffer.Status}, not Pending"); + + // Update lease offer + leaseOffer.Status = "Declined"; + leaseOffer.RespondedOn = DateTime.UtcNow; + leaseOffer.ResponseNotes = declineReason; + leaseOffer.LastModifiedBy = userId; + leaseOffer.LastModifiedOn = DateTime.UtcNow; + + // Update application + var application = leaseOffer.RentalApplication; + if (application != null) + { + application.Status = ApplicationConstants.ApplicationStatuses.LeaseDeclined; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.LeaseDeclined; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + } + + // Rollback property status + await RollbackPropertyStatusIfNeededAsync(leaseOffer.PropertyId); + + await LogTransitionAsync( + "LeaseOffer", + leaseOfferId, + "Pending", + "Declined", + "DeclineLeaseOffer", + declineReason); + + return WorkflowResult.Ok("Lease offer declined"); + + }); + } + + /// + /// Expires a lease offer (called by scheduled task). + /// Similar to decline but automated. + /// + public async Task ExpireLeaseOfferAsync(Guid leaseOfferId) + { + return await ExecuteWorkflowAsync(async () => + { + var orgId = await GetActiveOrganizationIdAsync(); + var userId = await GetCurrentUserIdAsync(); + + var leaseOffer = await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .ThenInclude(a => a.ProspectiveTenant) + .Include(lo => lo.Property) + .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && + lo.OrganizationId == orgId && + !lo.IsDeleted); + + if (leaseOffer == null) + return WorkflowResult.Fail("Lease offer not found"); + + if (leaseOffer.Status != "Pending") + return WorkflowResult.Fail($"Lease offer status is {leaseOffer.Status}, not Pending"); + + if (leaseOffer.ExpiresOn >= DateTime.UtcNow) + return WorkflowResult.Fail("Lease offer has not expired yet"); + + // Update lease offer + leaseOffer.Status = "Expired"; + leaseOffer.RespondedOn = DateTime.UtcNow; + leaseOffer.LastModifiedBy = userId; + leaseOffer.LastModifiedOn = DateTime.UtcNow; + + // Update application + var application = leaseOffer.RentalApplication; + if (application != null) + { + application.Status = ApplicationConstants.ApplicationStatuses.Expired; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.LeaseDeclined; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + } + + // Rollback property status + await RollbackPropertyStatusIfNeededAsync(leaseOffer.PropertyId); + + await LogTransitionAsync( + "LeaseOffer", + leaseOfferId, + "Pending", + "Expired", + "ExpireLeaseOffer", + "Offer expired after 30 days"); + + return WorkflowResult.Ok("Lease offer expired"); + + }); + } + #endregion #region Helper Methods - private async Task GetApplicationAsync(int applicationId) + private async Task GetApplicationAsync(Guid applicationId) { var orgId = await GetActiveOrganizationIdAsync(); return await _context.RentalApplications @@ -516,20 +1030,20 @@ await LogTransitionAsync( .Include(a => a.Screening) .FirstOrDefaultAsync(a => a.Id == applicationId && - a.OrganizationId == orgId.ToString() && + a.OrganizationId == orgId && !a.IsDeleted); } private async Task ValidateApplicationSubmissionAsync( - int prospectId, - int propertyId) + Guid prospectId, + Guid propertyId) { var errors = new List(); var orgId = await GetActiveOrganizationIdAsync(); // Validate prospect exists var prospect = await _context.ProspectiveTenants - .FirstOrDefaultAsync(p => p.Id == prospectId && p.OrganizationId == orgId.ToString() && !p.IsDeleted); + .FirstOrDefaultAsync(p => p.Id == prospectId && p.OrganizationId == orgId && !p.IsDeleted); if (prospect == null) errors.Add("Prospect not found"); @@ -538,7 +1052,7 @@ private async Task ValidateApplicationSubmissionAsync( // Validate property exists and is available var property = await _context.Properties - .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == orgId.ToString() && !p.IsDeleted); + .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == orgId && !p.IsDeleted); if (property == null) errors.Add("Property not found"); @@ -551,7 +1065,7 @@ private async Task ValidateApplicationSubmissionAsync( var existingApp = await _context.RentalApplications .AnyAsync(a => a.ProspectiveTenantId == prospectId && - a.OrganizationId == orgId.ToString() && + a.OrganizationId == orgId && a.Status != ApplicationConstants.ApplicationStatuses.Denied && a.Status != ApplicationConstants.ApplicationStatuses.Withdrawn && a.Status != ApplicationConstants.ApplicationStatuses.Expired && @@ -571,7 +1085,7 @@ private async Task ValidateApplicationSubmissionAsync( /// Checks if property status should roll back when an application is denied/withdrawn. /// Rolls back to Available if no active applications remain. /// - private async Task RollbackPropertyStatusIfNeededAsync(int propertyId) + private async Task RollbackPropertyStatusIfNeededAsync(Guid propertyId) { var orgId = await GetActiveOrganizationIdAsync(); var userId = await GetCurrentUserIdAsync(); @@ -588,7 +1102,7 @@ private async Task RollbackPropertyStatusIfNeededAsync(int propertyId) var hasActiveApplications = await _context.RentalApplications .AnyAsync(a => a.PropertyId == propertyId && - a.OrganizationId == orgId.ToString() && + a.OrganizationId == orgId && activeStates.Contains(a.Status) && !a.IsDeleted); @@ -596,7 +1110,7 @@ private async Task RollbackPropertyStatusIfNeededAsync(int propertyId) if (!hasActiveApplications) { var property = await _context.Properties - .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == orgId.ToString()); + .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == orgId); if (property != null && property.Status == ApplicationConstants.PropertyStatuses.ApplicationPending) { @@ -608,6 +1122,49 @@ private async Task RollbackPropertyStatusIfNeededAsync(int propertyId) } #endregion + + /// + /// Returns a comprehensive view of the application's workflow state, + /// including related prospect, property, screening, lease offers, and audit history. + /// + public async Task GetApplicationWorkflowStateAsync(Guid applicationId) + { + var orgId = await GetActiveOrganizationIdAsync(); + + var application = await _context.RentalApplications + .Include(a => a.ProspectiveTenant) + .Include(a => a.Property) + .Include(a => a.Screening) + .FirstOrDefaultAsync(a => a.Id == applicationId && a.OrganizationId == orgId && !a.IsDeleted); + + if (application == null) + return new ApplicationWorkflowState + { + Application = null, + AuditHistory = new List(), + LeaseOffers = new List() + }; + + var leaseOffers = await _context.LeaseOffers + .Where(lo => lo.RentalApplicationId == applicationId && lo.OrganizationId == orgId && !lo.IsDeleted) + .OrderByDescending(lo => lo.OfferedOn) + .ToListAsync(); + + var auditHistory = await _context.WorkflowAuditLogs + .Where(w => w.EntityType == "RentalApplication" && w.EntityId == applicationId && w.OrganizationId == orgId) + .OrderByDescending(w => w.PerformedOn) + .ToListAsync(); + + return new ApplicationWorkflowState + { + Application = application, + Prospect = application.ProspectiveTenant, + Property = application.Property, + Screening = application.Screening, + LeaseOffers = leaseOffers, + AuditHistory = auditHistory + }; + } } /// @@ -639,4 +1196,46 @@ public class ApplicationSubmissionModel public string? Reference2Phone { get; set; } public string? Reference2Relationship { get; set; } } + + /// + /// Model for screening results update. + /// + public class ScreeningResultModel + { + public bool? BackgroundCheckPassed { get; set; } + public string? BackgroundCheckNotes { get; set; } + + public bool? CreditCheckPassed { get; set; } + public int? CreditScore { get; set; } + public string? CreditCheckNotes { get; set; } + + public string OverallResult { get; set; } = "Pending"; // Pending, Passed, Failed, ConditionalPass + public string? ResultNotes { get; set; } + } + + /// + /// Model for lease offer generation. + /// + public class LeaseOfferModel + { + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public decimal MonthlyRent { get; set; } + public decimal SecurityDeposit { get; set; } + public string Terms { get; set; } = string.Empty; + public string? Notes { get; set; } + } + + /// + /// Aggregated workflow state returned by GetApplicationWorkflowStateAsync. + /// + public class ApplicationWorkflowState + { + public RentalApplication? Application { get; set; } + public ProspectiveTenant? Prospect { get; set; } + public Property? Property { get; set; } + public ApplicationScreening? Screening { get; set; } + public List LeaseOffers { get; set; } = new(); + public List AuditHistory { get; set; } = new(); + } } diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs index 2890833..669f696 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs @@ -43,6 +43,8 @@ protected async Task> ExecuteWorkflowAsync( else { await transaction.RollbackAsync(); + // Clear the ChangeTracker to discard all tracked changes + _context.ChangeTracker.Clear(); } return result; @@ -50,7 +52,21 @@ protected async Task> ExecuteWorkflowAsync( catch (Exception ex) { await transaction.RollbackAsync(); - return WorkflowResult.Fail($"Workflow operation failed: {ex.Message}"); + // Clear the ChangeTracker to discard all tracked changes + _context.ChangeTracker.Clear(); + + var errorMessage = ex.Message; + if (ex.InnerException != null) + { + errorMessage += $" | Inner: {ex.InnerException.Message}"; + if (ex.InnerException.InnerException != null) + { + errorMessage += $" | Inner(2): {ex.InnerException.InnerException.Message}"; + } + } + Console.WriteLine($"Workflow Error: {errorMessage}"); + Console.WriteLine($"Stack Trace: {ex.StackTrace}"); + return WorkflowResult.Fail($"Workflow operation failed: {errorMessage}"); } } @@ -74,6 +90,8 @@ protected async Task ExecuteWorkflowAsync( else { await transaction.RollbackAsync(); + // Clear the ChangeTracker to discard all tracked changes + _context.ChangeTracker.Clear(); } return result; @@ -81,7 +99,21 @@ protected async Task ExecuteWorkflowAsync( catch (Exception ex) { await transaction.RollbackAsync(); - return WorkflowResult.Fail($"Workflow operation failed: {ex.Message}"); + // Clear the ChangeTracker to discard all tracked changes + _context.ChangeTracker.Clear(); + + var errorMessage = ex.Message; + if (ex.InnerException != null) + { + errorMessage += $" | Inner: {ex.InnerException.Message}"; + if (ex.InnerException.InnerException != null) + { + errorMessage += $" | Inner(2): {ex.InnerException.InnerException.Message}"; + } + } + Console.WriteLine($"Workflow Error: {errorMessage}"); + Console.WriteLine($"Stack Trace: {ex.StackTrace}"); + return WorkflowResult.Fail($"Workflow operation failed: {errorMessage}"); } } @@ -90,7 +122,7 @@ protected async Task ExecuteWorkflowAsync( /// protected async Task LogTransitionAsync( string entityType, - int entityId, + Guid entityId, string? fromStatus, string toStatus, string action, @@ -102,6 +134,7 @@ protected async Task LogTransitionAsync( var auditLog = new WorkflowAuditLog { + Id = Guid.NewGuid(), EntityType = entityType, EntityId = entityId, FromStatus = fromStatus, @@ -110,7 +143,7 @@ protected async Task LogTransitionAsync( Reason = reason, PerformedBy = userId, PerformedOn = DateTime.UtcNow, - OrganizationId = int.Parse(activeOrgId ?? "0"), + OrganizationId = activeOrgId.HasValue ? activeOrgId.Value : Guid.Empty, Metadata = metadata != null ? JsonSerializer.Serialize(metadata) : null, CreatedOn = DateTime.UtcNow, CreatedBy = userId @@ -125,13 +158,13 @@ protected async Task LogTransitionAsync( /// public async Task> GetAuditHistoryAsync( string entityType, - int entityId) + Guid entityId) { var activeOrgId = await _userContext.GetActiveOrganizationIdAsync(); return await _context.WorkflowAuditLogs .Where(w => w.EntityType == entityType && w.EntityId == entityId) - .Where(w => w.OrganizationId == int.Parse(activeOrgId ?? "0")) + .Where(w => w.OrganizationId == activeOrgId) .OrderBy(w => w.PerformedOn) .ToListAsync(); } @@ -141,15 +174,15 @@ public async Task> GetAuditHistoryAsync( /// protected async Task ValidateOrganizationOwnershipAsync( IQueryable query, - int entityId) where TEntity : class + Guid entityId) where TEntity : class { var activeOrgId = await _userContext.GetActiveOrganizationIdAsync(); // This assumes entities have OrganizationId property // Override in derived classes if different validation needed var entity = await query - .Where(e => EF.Property(e, "Id") == entityId) - .Where(e => EF.Property(e, "OrganizationId") == int.Parse(activeOrgId ?? "0")) + .Where(e => EF.Property(e, "Id") == entityId) + .Where(e => EF.Property(e, "OrganizationId") == activeOrgId) .Where(e => EF.Property(e, "IsDeleted") == false) .FirstOrDefaultAsync(); @@ -167,10 +200,9 @@ protected async Task GetCurrentUserIdAsync() /// /// Gets the active organization ID from the user context. /// - protected async Task GetActiveOrganizationIdAsync() + protected async Task GetActiveOrganizationIdAsync() { - var activeOrgId = await _userContext.GetActiveOrganizationIdAsync(); - return int.Parse(activeOrgId ?? "0"); + return await _userContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; } } } diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs b/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs index f115ba5..07b3c8e 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs @@ -16,10 +16,7 @@ public class WorkflowAuditLog : BaseModel /// /// ID of the entity that transitioned /// - public required int EntityId { get; set; } - - /// - /// Previous status (null for initial creation) + public required Guid EntityId { get; set; } /// public string? FromStatus { get; set; } @@ -51,7 +48,7 @@ public class WorkflowAuditLog : BaseModel /// /// Organization context for the workflow action /// - public required int OrganizationId { get; set; } + public required Guid OrganizationId { get; set; } /// /// Additional context data (JSON serialized) diff --git a/Aquiis.SimpleStart/Aquiis.SimpleStart.csproj b/Aquiis.SimpleStart/Aquiis.SimpleStart.csproj index 439b7c2..f4b95d3 100644 --- a/Aquiis.SimpleStart/Aquiis.SimpleStart.csproj +++ b/Aquiis.SimpleStart/Aquiis.SimpleStart.csproj @@ -6,6 +6,7 @@ enable aspnet-Aquiis.SimpleStart-c69b6efe-bb20-41de-8cba-044207ebdce1 true + Infrastructure/Data/Migrations diff --git a/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs b/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs index e38814e..1556caf 100644 --- a/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs +++ b/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs @@ -235,17 +235,35 @@ public static class LeaseTypes { public static class LeaseStatuses { public const string Offered = "Offered"; public const string Pending = "Pending"; + public const string Accepted = "Accepted"; + public const string AcceptedPendingStart = "Accepted - Pending Start"; public const string Active = "Active"; public const string Declined = "Declined"; + public const string Renewed = "Renewed"; + public const string Interrupted = "Interrupted"; public const string Terminated = "Terminated"; public const string Expired = "Expired"; + public static IReadOnlyList RenewalStatuses { get; } = new List + { + "NotRequired", + "Pending", + "Offered", + "Accepted", + "Declined", + "Expired" + }; + public static IReadOnlyList AllLeaseStatuses { get; } = new List { Offered, Pending, + Accepted, + AcceptedPendingStart, Active, Declined, + Renewed, + Interrupted, Terminated, Expired }; @@ -281,12 +299,20 @@ public static class PropertyTypes public static class PropertyStatuses { public const string Available = "Available"; - public const string ApplicationPending = "ApplicationPending"; - public const string LeasePending = "LeasePending"; + public const string ApplicationPending = "Application Pending"; + public const string LeasePending = "Lease Pending"; + public const string MoveInPending = "Accepted - Move-In Pending"; public const string Occupied = "Occupied"; - public const string UnderRenovation = "UnderRenovation"; - public const string OffMarket = "OffMarket"; + public const string MoveOutPending = "Move-Out Pending"; + public const string UnderRenovation = "Under Renovation"; + public const string OffMarket = "Off Market"; + public static IReadOnlyList OccupiedStatuses { get; } = new List + { + MoveInPending, + Occupied, + MoveOutPending + }; public static IReadOnlyList AllPropertyStatuses { get; } = new List { Available, @@ -359,16 +385,22 @@ public static class MaintenanceRequestStatuses public static class TenantStatuses { + public const string Prospective = "Prospective"; + public const string Pending = "Pending"; + public const string MoveInPending = "Move-In Pending"; public const string Active = "Active"; + public const string MoveOutPending = "Move-Out Pending"; public const string Inactive = "Inactive"; - public const string Prospective = "Prospective"; public const string Evicted = "Evicted"; public static IReadOnlyList AllTenantStatuses { get; } = new List - { + { + Prospective, + Pending, + MoveInPending, Active, + MoveOutPending, Inactive, - Prospective, Evicted }; @@ -432,15 +464,15 @@ public static class ChecklistStatuses public static class ProspectiveStatuses { public const string Lead = "Lead"; - public const string TourScheduled = "TourScheduled"; + public const string TourScheduled = "Tour Scheduled"; public const string Applied = "Applied"; public const string Screening = "Screening"; public const string Approved = "Approved"; public const string Denied = "Denied"; public const string Withdrawn = "Withdrawn"; - public const string LeaseOffered = "LeaseOffered"; - public const string LeaseDeclined = "LeaseDeclined"; - public const string ConvertedToTenant = "ConvertedToTenant"; + public const string LeaseOffered = "Lease Offered"; + public const string LeaseDeclined = "Lease Declined"; + public const string ConvertedToTenant = "Converted To Tenant"; public static IReadOnlyList AllProspectiveStatuses { get; } = new List { @@ -499,10 +531,10 @@ public static class TourStatuses public static class TourInterestLevels { - public const string VeryInterested = "VeryInterested"; + public const string VeryInterested = "Very Interested"; public const string Interested = "Interested"; public const string Neutral = "Neutral"; - public const string NotInterested = "NotInterested"; + public const string NotInterested = "Not Interested"; public static IReadOnlyList AllTourInterestLevels { get; } = new List { @@ -516,15 +548,15 @@ public static class TourInterestLevels public static class ApplicationStatuses { public const string Submitted = "Submitted"; - public const string UnderReview = "UnderReview"; + public const string UnderReview = "Under Review"; public const string Screening = "Screening"; public const string Approved = "Approved"; public const string Denied = "Denied"; public const string Expired = "Expired"; public const string Withdrawn = "Withdrawn"; - public const string LeaseOffered = "LeaseOffered"; - public const string LeaseAccepted = "LeaseAccepted"; - public const string LeaseDeclined = "LeaseDeclined"; + public const string LeaseOffered = "Lease Offered"; + public const string LeaseAccepted = "Lease Accepted"; + public const string LeaseDeclined = "Lease Declined"; public static IReadOnlyList AllApplicationStatuses { get; } = new List { @@ -546,7 +578,7 @@ public static class ScreeningResults public const string Pending = "Pending"; public const string Passed = "Passed"; public const string Failed = "Failed"; - public const string ConditionalPass = "ConditionalPass"; + public const string ConditionalPass = "Conditional Pass"; public static IReadOnlyList AllScreeningResults { get; } = new List { @@ -563,7 +595,7 @@ public static class SecurityDepositStatuses public const string Released = "Released"; public const string Refunded = "Refunded"; public const string Forfeited = "Forfeited"; - public const string PartiallyRefunded = "PartiallyRefunded"; + public const string PartiallyRefunded = "Partially Refunded"; public static IReadOnlyList AllSecurityDepositStatuses { get; } = new List { @@ -594,7 +626,7 @@ public static class InvestmentPoolStatuses public static class DividendPaymentMethods { public const string Pending = "Pending"; - public const string LeaseCredit = "LeaseCredit"; + public const string LeaseCredit = "Lease Credit"; public const string Check = "Check"; public static IReadOnlyList AllDividendPaymentMethods { get; } = new List @@ -608,7 +640,7 @@ public static class DividendPaymentMethods public static class DividendStatuses { public const string Pending = "Pending"; - public const string ChoiceMade = "ChoiceMade"; + public const string ChoiceMade = "Choice Made"; public const string Applied = "Applied"; public const string Paid = "Paid"; @@ -621,6 +653,23 @@ public static class DividendStatuses }; } + public static class EntityTypes + { + public const string Property = "Property"; + public const string Tenant = "Tenant"; + public const string Lease = "Lease"; + public const string Invoice = "Invoice"; + public const string Payment = "Payment"; + public const string MaintenanceRequest = "MaintenanceRequest"; + public const string Document = "Document"; + public const string Inspection = "Inspection"; + public const string ProspectiveTenant = "ProspectiveTenant"; + public const string Application = "Application"; + public const string Tour = "Tour"; + public const string Checklist = "Checklist"; + public const string Note = "Note"; + } + } diff --git a/Aquiis.SimpleStart/Core/Constants/EntityTypeNames.cs b/Aquiis.SimpleStart/Core/Constants/EntityTypeNames.cs new file mode 100644 index 0000000..53c1588 --- /dev/null +++ b/Aquiis.SimpleStart/Core/Constants/EntityTypeNames.cs @@ -0,0 +1,65 @@ +using Aquiis.SimpleStart.Core.Entities; + +namespace Aquiis.SimpleStart.Core.Constants; + +/// +/// Centralized entity type names for integration tables (Notes, Audit Logs, etc.) +/// Uses fully-qualified type names to prevent collisions with external systems +/// +public static class EntityTypeNames +{ + // Property Management Domain + public const string Property = "Aquiis.SimpleStart.Core.Entities.Property"; + public const string Tenant = "Aquiis.SimpleStart.Core.Entities.Tenant"; + public const string Lease = "Aquiis.SimpleStart.Core.Entities.Lease"; + public const string LeaseOffer = "Aquiis.SimpleStart.Core.Entities.LeaseOffer"; + public const string Invoice = "Aquiis.SimpleStart.Core.Entities.Invoice"; + public const string Payment = "Aquiis.SimpleStart.Core.Entities.Payment"; + public const string MaintenanceRequest = "Aquiis.SimpleStart.Core.Entities.MaintenanceRequest"; + public const string Inspection = "Aquiis.SimpleStart.Core.Entities.Inspection"; + public const string Document = "Aquiis.SimpleStart.Core.Entities.Document"; + + // Application/Prospect Domain + public const string ProspectiveTenant = "Aquiis.SimpleStart.Core.Entities.ProspectiveTenant"; + public const string Application = "Aquiis.SimpleStart.Core.Entities.Application"; + public const string Tour = "Aquiis.SimpleStart.Core.Entities.Tour"; + + // Checklist Domain + public const string Checklist = "Aquiis.SimpleStart.Core.Entities.Checklist"; + public const string ChecklistTemplate = "Aquiis.SimpleStart.Core.Entities.ChecklistTemplate"; + + // Calendar/Events + public const string CalendarEvent = "Aquiis.SimpleStart.Core.Entities.CalendarEvent"; + + // Security Deposits + public const string SecurityDepositPool = "Aquiis.SimpleStart.Core.Entities.SecurityDepositPool"; + public const string SecurityDepositTransaction = "Aquiis.SimpleStart.Core.Entities.SecurityDepositTransaction"; + + /// + /// Get the fully-qualified type name for an entity type + /// + public static string GetTypeName() where T : BaseModel + { + return typeof(T).FullName ?? typeof(T).Name; + } + + /// + /// Get the display name (simple name) from a fully-qualified type name + /// + public static string GetDisplayName(string fullyQualifiedName) + { + return fullyQualifiedName.Split('.').Last(); + } + + /// + /// Validate that an entity type string is recognized + /// + public static bool IsValidEntityType(string entityType) + { + return typeof(EntityTypeNames) + .GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static) + .Where(f => f.FieldType == typeof(string)) + .Select(f => f.GetValue(null) as string) + .Contains(entityType); + } +} diff --git a/Aquiis.SimpleStart/Core/Entities/ApplicationScreening.cs b/Aquiis.SimpleStart/Core/Entities/ApplicationScreening.cs index 87e4e46..3d1d666 100644 --- a/Aquiis.SimpleStart/Core/Entities/ApplicationScreening.cs +++ b/Aquiis.SimpleStart/Core/Entities/ApplicationScreening.cs @@ -1,18 +1,18 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class ApplicationScreening : BaseModel { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] + [RequiredGuid] [Display(Name = "Rental Application")] - public int RentalApplicationId { get; set; } + public Guid RentalApplicationId { get; set; } // Background Check [Display(Name = "Background Check Requested")] diff --git a/Aquiis.SimpleStart/Core/Entities/BaseModel.cs b/Aquiis.SimpleStart/Core/Entities/BaseModel.cs index 52b6ddb..89ffb35 100644 --- a/Aquiis.SimpleStart/Core/Entities/BaseModel.cs +++ b/Aquiis.SimpleStart/Core/Entities/BaseModel.cs @@ -8,8 +8,8 @@ public class BaseModel { [Key] [JsonInclude] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Id { get; set; } [Required] [JsonInclude] diff --git a/Aquiis.SimpleStart/Core/Entities/CalendarEvent.cs b/Aquiis.SimpleStart/Core/Entities/CalendarEvent.cs index cb92485..3b60c09 100644 --- a/Aquiis.SimpleStart/Core/Entities/CalendarEvent.cs +++ b/Aquiis.SimpleStart/Core/Entities/CalendarEvent.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { @@ -9,10 +10,9 @@ namespace Aquiis.SimpleStart.Core.Entities /// public class CalendarEvent : BaseModel { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] [StringLength(200)] @@ -43,7 +43,7 @@ public class CalendarEvent : BaseModel public string? Description { get; set; } [Display(Name = "Property")] - public int? PropertyId { get; set; } + public Guid? PropertyId { get; set; } [StringLength(500)] [Display(Name = "Location")] @@ -59,7 +59,7 @@ public class CalendarEvent : BaseModel // Polymorphic reference to source entity (null for custom events) [Display(Name = "Source Entity ID")] - public int? SourceEntityId { get; set; } + public Guid? SourceEntityId { get; set; } [StringLength(100)] [Display(Name = "Source Entity Type")] diff --git a/Aquiis.SimpleStart/Core/Entities/CalendarSettings.cs b/Aquiis.SimpleStart/Core/Entities/CalendarSettings.cs index f4c9cdc..e9cc02a 100644 --- a/Aquiis.SimpleStart/Core/Entities/CalendarSettings.cs +++ b/Aquiis.SimpleStart/Core/Entities/CalendarSettings.cs @@ -1,13 +1,13 @@ using System.ComponentModel.DataAnnotations; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities; public class CalendarSettings : BaseModel { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + 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/Aquiis.SimpleStart/Core/Entities/Checklist.cs b/Aquiis.SimpleStart/Core/Entities/Checklist.cs index 6428666..ba8970e 100644 --- a/Aquiis.SimpleStart/Core/Entities/Checklist.cs +++ b/Aquiis.SimpleStart/Core/Entities/Checklist.cs @@ -1,24 +1,24 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class Checklist : BaseModel { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Display(Name = "Property ID")] - public int? PropertyId { get; set; } + public Guid? PropertyId { get; set; } [Display(Name = "Lease ID")] - public int? LeaseId { get; set; } + public Guid? LeaseId { get; set; } - [Required] + [RequiredGuid] [Display(Name = "Checklist Template ID")] - public int ChecklistTemplateId { get; set; } + public Guid ChecklistTemplateId { get; set; } [Required] [StringLength(200)] @@ -43,7 +43,7 @@ public class Checklist : BaseModel public DateTime? CompletedOn { get; set; } [Display(Name = "Document ID")] - public int? DocumentId { get; set; } + public Guid? DocumentId { get; set; } [StringLength(2000)] [Display(Name = "General Notes")] diff --git a/Aquiis.SimpleStart/Core/Entities/ChecklistItem.cs b/Aquiis.SimpleStart/Core/Entities/ChecklistItem.cs index 9df990f..1615727 100644 --- a/Aquiis.SimpleStart/Core/Entities/ChecklistItem.cs +++ b/Aquiis.SimpleStart/Core/Entities/ChecklistItem.cs @@ -1,19 +1,19 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class ChecklistItem : BaseModel { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] + [RequiredGuid] [Display(Name = "Checklist ID")] - public int ChecklistId { get; set; } + public Guid ChecklistId { get; set; } [Required] [StringLength(500)] diff --git a/Aquiis.SimpleStart/Core/Entities/ChecklistTemplate.cs b/Aquiis.SimpleStart/Core/Entities/ChecklistTemplate.cs index e03c81d..b427376 100644 --- a/Aquiis.SimpleStart/Core/Entities/ChecklistTemplate.cs +++ b/Aquiis.SimpleStart/Core/Entities/ChecklistTemplate.cs @@ -1,13 +1,13 @@ using System.ComponentModel.DataAnnotations; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class ChecklistTemplate : BaseModel { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] [StringLength(100)] diff --git a/Aquiis.SimpleStart/Core/Entities/ChecklistTemplateItem.cs b/Aquiis.SimpleStart/Core/Entities/ChecklistTemplateItem.cs index 6c51a3c..8d95ff6 100644 --- a/Aquiis.SimpleStart/Core/Entities/ChecklistTemplateItem.cs +++ b/Aquiis.SimpleStart/Core/Entities/ChecklistTemplateItem.cs @@ -1,18 +1,18 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class ChecklistTemplateItem : BaseModel { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] + [RequiredGuid] [Display(Name = "Checklist Template ID")] - public int ChecklistTemplateId { get; set; } + public Guid ChecklistTemplateId { get; set; } [Required] [StringLength(500)] diff --git a/Aquiis.SimpleStart/Core/Entities/Document.cs b/Aquiis.SimpleStart/Core/Entities/Document.cs index c9c4400..e270a35 100644 --- a/Aquiis.SimpleStart/Core/Entities/Document.cs +++ b/Aquiis.SimpleStart/Core/Entities/Document.cs @@ -9,7 +9,7 @@ public class Document:BaseModel [Required] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] [StringLength(255)] @@ -42,11 +42,11 @@ public class Document:BaseModel public string Description { get; set; } = string.Empty; // Foreign keys - at least one must be set - public int? PropertyId { get; set; } - public int? TenantId { get; set; } - public int? LeaseId { get; set; } - public int? InvoiceId { get; set; } - public int? PaymentId { get; set; } + public Guid? PropertyId { get; set; } + public Guid? TenantId { get; set; } + public Guid? LeaseId { get; set; } + public Guid? InvoiceId { get; set; } + public Guid? PaymentId { get; set; } // Navigation properties [ForeignKey("PropertyId")] diff --git a/Aquiis.SimpleStart/Core/Entities/ISchedulableEntity.cs b/Aquiis.SimpleStart/Core/Entities/ISchedulableEntity.cs index 991d8cc..f3a9d6b 100644 --- a/Aquiis.SimpleStart/Core/Entities/ISchedulableEntity.cs +++ b/Aquiis.SimpleStart/Core/Entities/ISchedulableEntity.cs @@ -9,12 +9,12 @@ public interface ISchedulableEntity /// /// Entity ID /// - int Id { get; set; } + Guid Id { get; set; } /// /// Organization ID /// - string OrganizationId { get; set; } + Guid OrganizationId { get; set; } /// /// Created By User ID @@ -24,7 +24,7 @@ public interface ISchedulableEntity /// /// Link to the associated CalendarEvent /// - int? CalendarEventId { get; set; } + Guid? CalendarEventId { get; set; } /// /// Get the title to display on the calendar @@ -49,7 +49,7 @@ public interface ISchedulableEntity /// /// Get the associated property ID (if applicable) /// - int? GetPropertyId(); + Guid? GetPropertyId(); /// /// Get the description/details for the event diff --git a/Aquiis.SimpleStart/Core/Entities/IncomeStatement.cs b/Aquiis.SimpleStart/Core/Entities/IncomeStatement.cs index c6b59ab..9047867 100644 --- a/Aquiis.SimpleStart/Core/Entities/IncomeStatement.cs +++ b/Aquiis.SimpleStart/Core/Entities/IncomeStatement.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities; @@ -7,14 +8,13 @@ namespace Aquiis.SimpleStart.Core.Entities; /// public class IncomeStatement { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } - public int? PropertyId { get; set; } + public Guid? PropertyId { get; set; } public string? PropertyName { get; set; } // Income @@ -42,10 +42,11 @@ public class IncomeStatement /// public class RentRollItem { - public int PropertyId { get; set; } + [RequiredGuid] + public Guid PropertyId { get; set; } public string PropertyName { get; set; } = string.Empty; public string PropertyAddress { get; set; } = string.Empty; - public int? TenantId { get; set; } + public Guid? TenantId { get; set; } public string? TenantName { get; set; } public string LeaseStatus { get; set; } = string.Empty; public DateTime? LeaseStartDate { get; set; } @@ -63,7 +64,8 @@ public class RentRollItem /// public class PropertyPerformance { - public int PropertyId { get; set; } + [RequiredGuid] + public Guid PropertyId { get; set; } public string PropertyName { get; set; } = string.Empty; public string PropertyAddress { get; set; } = string.Empty; public decimal TotalIncome { get; set; } @@ -81,7 +83,7 @@ public class PropertyPerformance public class TaxReportData { public int Year { get; set; } - public int? PropertyId { get; set; } + public Guid? PropertyId { get; set; } public string? PropertyName { get; set; } public decimal TotalRentIncome { get; set; } public decimal TotalExpenses { get; set; } diff --git a/Aquiis.SimpleStart/Core/Entities/Inspection.cs b/Aquiis.SimpleStart/Core/Entities/Inspection.cs index 4e882a5..2b47629 100644 --- a/Aquiis.SimpleStart/Core/Entities/Inspection.cs +++ b/Aquiis.SimpleStart/Core/Entities/Inspection.cs @@ -9,14 +9,14 @@ public class Inspection : BaseModel, ISchedulableEntity [Required] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] - public int PropertyId { get; set; } + public Guid PropertyId { get; set; } - public int? CalendarEventId { get; set; } + public Guid? CalendarEventId { get; set; } - public int? LeaseId { get; set; } + public Guid? LeaseId { get; set; } [Required] public DateTime CompletedOn { get; set; } = DateTime.Now; @@ -120,7 +120,7 @@ public class Inspection : BaseModel, ISchedulableEntity public string? ActionItemsRequired { get; set; } // Generated PDF Document - public int? DocumentId { get; set; } + public Guid? DocumentId { get; set; } // Navigation Properties [ForeignKey("PropertyId")] @@ -144,7 +144,7 @@ public class Inspection : BaseModel, ISchedulableEntity public string GetEventType() => CalendarEventTypes.Inspection; - public int? GetPropertyId() => PropertyId; + public Guid? GetPropertyId() => PropertyId; public string GetEventDescription() => $"{InspectionType} - {OverallCondition}"; diff --git a/Aquiis.SimpleStart/Core/Entities/Invoice.cs b/Aquiis.SimpleStart/Core/Entities/Invoice.cs index 5122590..65ba985 100644 --- a/Aquiis.SimpleStart/Core/Entities/Invoice.cs +++ b/Aquiis.SimpleStart/Core/Entities/Invoice.cs @@ -9,10 +9,10 @@ public class Invoice : BaseModel [Required] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] - public int LeaseId { get; set; } + public Guid LeaseId { get; set; } [Required] [StringLength(50)] @@ -59,7 +59,7 @@ public class Invoice : BaseModel public DateTime? ReminderSentOn { get; set; } // Document Tracking - public int? DocumentId { get; set; } + public Guid? DocumentId { get; set; } // Navigation properties [ForeignKey("LeaseId")] diff --git a/Aquiis.SimpleStart/Core/Entities/Lease.cs b/Aquiis.SimpleStart/Core/Entities/Lease.cs index 97878e1..a7c1612 100644 --- a/Aquiis.SimpleStart/Core/Entities/Lease.cs +++ b/Aquiis.SimpleStart/Core/Entities/Lease.cs @@ -1,24 +1,24 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class Lease : BaseModel { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] - public int PropertyId { get; set; } + [RequiredGuid] + public Guid PropertyId { get; set; } - [Required] - public int TenantId { get; set; } + [RequiredGuid] + public Guid TenantId { get; set; } // Reference to the lease offer if this lease was created from an accepted offer - public int? LeaseOfferId { get; set; } + public Guid? LeaseOfferId { get; set; } [Required] [DataType(DataType.Date)] @@ -74,10 +74,10 @@ public class Lease : BaseModel public string? RenewalNotes { get; set; } // Lease Chain Tracking - public int? PreviousLeaseId { get; set; } + public Guid? PreviousLeaseId { get; set; } // Document Tracking - public int? DocumentId { get; set; } + public Guid? DocumentId { get; set; } // Navigation properties [ForeignKey("PropertyId")] diff --git a/Aquiis.SimpleStart/Core/Entities/LeaseOffer.cs b/Aquiis.SimpleStart/Core/Entities/LeaseOffer.cs index a628459..c6f424e 100644 --- a/Aquiis.SimpleStart/Core/Entities/LeaseOffer.cs +++ b/Aquiis.SimpleStart/Core/Entities/LeaseOffer.cs @@ -8,16 +8,16 @@ public class LeaseOffer : BaseModel [Required] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] - public int RentalApplicationId { get; set; } + public Guid RentalApplicationId { get; set; } [Required] - public int PropertyId { get; set; } + public Guid PropertyId { get; set; } [Required] - public int ProspectiveTenantId { get; set; } + public Guid ProspectiveTenantId { get; set; } [Required] [DataType(DataType.Date)] @@ -56,7 +56,7 @@ public class LeaseOffer : BaseModel [StringLength(500)] public string? ResponseNotes { get; set; } - public int? ConvertedLeaseId { get; set; } // Set when offer is accepted and converted to lease + public Guid? ConvertedLeaseId { get; set; } // Set when offer is accepted and converted to lease // Navigation properties [ForeignKey("RentalApplicationId")] diff --git a/Aquiis.SimpleStart/Core/Entities/MaintenanceRequest.cs b/Aquiis.SimpleStart/Core/Entities/MaintenanceRequest.cs index 304c029..da3ff1c 100644 --- a/Aquiis.SimpleStart/Core/Entities/MaintenanceRequest.cs +++ b/Aquiis.SimpleStart/Core/Entities/MaintenanceRequest.cs @@ -1,21 +1,21 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class MaintenanceRequest : BaseModel, ISchedulableEntity { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] - public int PropertyId { get; set; } + [RequiredGuid] + public Guid PropertyId { get; set; } - public int? CalendarEventId { get; set; } + public Guid? CalendarEventId { get; set; } - public int? LeaseId { get; set; } + public Guid? LeaseId { get; set; } [Required] [StringLength(100)] @@ -136,7 +136,7 @@ public string StatusBadgeClass public string GetEventType() => CalendarEventTypes.Maintenance; - public int? GetPropertyId() => PropertyId; + public Guid? GetPropertyId() => PropertyId; public string GetEventDescription() => $"{Property?.Address ?? "Property"} - {Priority} Priority"; diff --git a/Aquiis.SimpleStart/Core/Entities/Note.cs b/Aquiis.SimpleStart/Core/Entities/Note.cs index 15b3c18..a1d74b9 100644 --- a/Aquiis.SimpleStart/Core/Entities/Note.cs +++ b/Aquiis.SimpleStart/Core/Entities/Note.cs @@ -12,7 +12,7 @@ public class Note : BaseModel [Required] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] [StringLength(5000)] @@ -26,7 +26,7 @@ public class Note : BaseModel [Required] [Display(Name = "Entity ID")] - public int EntityId { get; set; } + public Guid EntityId { get; set; } [StringLength(100)] [Display(Name = "User Full Name")] diff --git a/Aquiis.SimpleStart/Core/Entities/Organization.cs b/Aquiis.SimpleStart/Core/Entities/Organization.cs index d7bbe80..33d14f6 100644 --- a/Aquiis.SimpleStart/Core/Entities/Organization.cs +++ b/Aquiis.SimpleStart/Core/Entities/Organization.cs @@ -1,13 +1,13 @@ using System.ComponentModel.DataAnnotations; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class Organization { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string Id { get; set; } = Guid.NewGuid().ToString(); + public Guid Id { get; set; } = Guid.Empty; /// /// UserId of the account owner who created this organization diff --git a/Aquiis.SimpleStart/Core/Entities/OrganizationSettings.cs b/Aquiis.SimpleStart/Core/Entities/OrganizationSettings.cs index 15167c5..7f45afc 100644 --- a/Aquiis.SimpleStart/Core/Entities/OrganizationSettings.cs +++ b/Aquiis.SimpleStart/Core/Entities/OrganizationSettings.cs @@ -14,7 +14,7 @@ public class OrganizationSettings : BaseModel [Required] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [MaxLength(200)] public string? Name { get; set; } diff --git a/Aquiis.SimpleStart/Core/Entities/Payment.cs b/Aquiis.SimpleStart/Core/Entities/Payment.cs index 09d2832..a3403aa 100644 --- a/Aquiis.SimpleStart/Core/Entities/Payment.cs +++ b/Aquiis.SimpleStart/Core/Entities/Payment.cs @@ -8,10 +8,10 @@ public class Payment : BaseModel [Required] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] - public int InvoiceId { get; set; } + public Guid InvoiceId { get; set; } [Required] [DataType(DataType.Date)] @@ -28,7 +28,7 @@ public class Payment : BaseModel public string Notes { get; set; } = string.Empty; // Document Tracking - public int? DocumentId { get; set; } + public Guid? DocumentId { get; set; } // Navigation properties [ForeignKey("InvoiceId")] diff --git a/Aquiis.SimpleStart/Core/Entities/Property.cs b/Aquiis.SimpleStart/Core/Entities/Property.cs index 6720959..2b9136c 100644 --- a/Aquiis.SimpleStart/Core/Entities/Property.cs +++ b/Aquiis.SimpleStart/Core/Entities/Property.cs @@ -12,7 +12,7 @@ public class Property : BaseModel [StringLength(100)] [DataType(DataType.Text)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] [JsonInclude] diff --git a/Aquiis.SimpleStart/Core/Entities/ProspectiveTenant.cs b/Aquiis.SimpleStart/Core/Entities/ProspectiveTenant.cs index baf7dbd..e954a24 100644 --- a/Aquiis.SimpleStart/Core/Entities/ProspectiveTenant.cs +++ b/Aquiis.SimpleStart/Core/Entities/ProspectiveTenant.cs @@ -8,7 +8,7 @@ public class ProspectiveTenant : BaseModel [Required] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] [StringLength(100)] @@ -58,7 +58,7 @@ public class ProspectiveTenant : BaseModel public string? Notes { get; set; } [Display(Name = "Interested Property")] - public int? InterestedPropertyId { get; set; } + public Guid? InterestedPropertyId { get; set; } [Display(Name = "Desired Move-In Date")] public DateTime? DesiredMoveInDate { get; set; } diff --git a/Aquiis.SimpleStart/Core/Entities/RentalApplication.cs b/Aquiis.SimpleStart/Core/Entities/RentalApplication.cs index aac6642..53066bd 100644 --- a/Aquiis.SimpleStart/Core/Entities/RentalApplication.cs +++ b/Aquiis.SimpleStart/Core/Entities/RentalApplication.cs @@ -11,15 +11,15 @@ public class RentalApplication : BaseModel [StringLength(100)] [DataType(DataType.Text)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] [Display(Name = "Prospective Tenant")] - public int ProspectiveTenantId { get; set; } + public Guid ProspectiveTenantId { get; set; } [Required] [Display(Name = "Property")] - public int PropertyId { get; set; } + public Guid PropertyId { get; set; } [Required] [Display(Name = "Applied On")] diff --git a/Aquiis.SimpleStart/Core/Entities/SecurityDeposit.cs b/Aquiis.SimpleStart/Core/Entities/SecurityDeposit.cs index 5602468..995be90 100644 --- a/Aquiis.SimpleStart/Core/Entities/SecurityDeposit.cs +++ b/Aquiis.SimpleStart/Core/Entities/SecurityDeposit.cs @@ -14,15 +14,15 @@ public class SecurityDeposit : BaseModel [JsonInclude] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] [JsonInclude] - public int LeaseId { get; set; } + public Guid LeaseId { get; set; } [Required] [JsonInclude] - public int TenantId { get; set; } + public Guid TenantId { get; set; } [Required] [Column(TypeName = "decimal(18,2)")] diff --git a/Aquiis.SimpleStart/Core/Entities/SecurityDepositDividend.cs b/Aquiis.SimpleStart/Core/Entities/SecurityDepositDividend.cs index 505ba9a..77f3c38 100644 --- a/Aquiis.SimpleStart/Core/Entities/SecurityDepositDividend.cs +++ b/Aquiis.SimpleStart/Core/Entities/SecurityDepositDividend.cs @@ -14,19 +14,19 @@ public class SecurityDepositDividend : BaseModel [JsonInclude] [StringLength(100)] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] - public int SecurityDepositId { get; set; } + public Guid SecurityDepositId { get; set; } [Required] - public int InvestmentPoolId { get; set; } + public Guid InvestmentPoolId { get; set; } [Required] - public int LeaseId { get; set; } + public Guid LeaseId { get; set; } [Required] - public int TenantId { get; set; } + public Guid TenantId { get; set; } [Required] public int Year { get; set; } diff --git a/Aquiis.SimpleStart/Core/Entities/SecurityDepositInvestmentPool.cs b/Aquiis.SimpleStart/Core/Entities/SecurityDepositInvestmentPool.cs index 24719ad..393aad9 100644 --- a/Aquiis.SimpleStart/Core/Entities/SecurityDepositInvestmentPool.cs +++ b/Aquiis.SimpleStart/Core/Entities/SecurityDepositInvestmentPool.cs @@ -10,7 +10,7 @@ namespace Aquiis.SimpleStart.Core.Entities public class SecurityDepositInvestmentPool : BaseModel { [Required] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] public int Year { get; set; } diff --git a/Aquiis.SimpleStart/Core/Entities/Tenant.cs b/Aquiis.SimpleStart/Core/Entities/Tenant.cs index c43e932..18408e6 100644 --- a/Aquiis.SimpleStart/Core/Entities/Tenant.cs +++ b/Aquiis.SimpleStart/Core/Entities/Tenant.cs @@ -1,12 +1,13 @@ using System.ComponentModel.DataAnnotations; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class Tenant : BaseModel { - [Required] - public string OrganizationId { get; set; } = string.Empty; + [RequiredGuid] + public Guid OrganizationId { get; set; } = Guid.Empty; [Required] [StringLength(100)] @@ -45,7 +46,7 @@ public class Tenant : BaseModel public string Notes { get; set; } = string.Empty; // Link back to prospect for audit trail - public int? ProspectiveTenantId { get; set; } + public Guid? ProspectiveTenantId { get; set; } // Navigation properties public virtual ICollection Leases { get; set; } = new List(); diff --git a/Aquiis.SimpleStart/Core/Entities/Tour.cs b/Aquiis.SimpleStart/Core/Entities/Tour.cs index 3a9de9d..2ec1a09 100644 --- a/Aquiis.SimpleStart/Core/Entities/Tour.cs +++ b/Aquiis.SimpleStart/Core/Entities/Tour.cs @@ -1,22 +1,22 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { public class Tour : BaseModel, ISchedulableEntity { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "Organization ID")] - public string OrganizationId { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } = Guid.Empty; - [Required] + [RequiredGuid] [Display(Name = "Prospective Tenant")] - public int ProspectiveTenantId { get; set; } + public Guid ProspectiveTenantId { get; set; } - [Required] + [RequiredGuid] [Display(Name = "Property")] - public int PropertyId { get; set; } + public Guid PropertyId { get; set; } [Required] [Display(Name = "Scheduled Date & Time")] @@ -42,10 +42,10 @@ public class Tour : BaseModel, ISchedulableEntity public string? ConductedBy { get; set; } = string.Empty; // UserId of property manager [Display(Name = "Property Tour Checklist")] - public int? ChecklistId { get; set; } // Links to property tour checklist + public Guid? ChecklistId { get; set; } // Links to property tour checklist [Display(Name = "Calendar Event")] - public int? CalendarEventId { get; set; } + public Guid? CalendarEventId { get; set; } // Navigation properties [ForeignKey(nameof(ProspectiveTenantId))] @@ -66,7 +66,7 @@ public class Tour : BaseModel, ISchedulableEntity public string GetEventType() => CalendarEventTypes.Tour; - public int? GetPropertyId() => PropertyId; + public Guid? GetPropertyId() => PropertyId; public string GetEventDescription() => Property?.Address ?? string.Empty; diff --git a/Aquiis.SimpleStart/Core/Entities/UserOrganization.cs b/Aquiis.SimpleStart/Core/Entities/UserOrganization.cs index bc7b112..b8fd311 100644 --- a/Aquiis.SimpleStart/Core/Entities/UserOrganization.cs +++ b/Aquiis.SimpleStart/Core/Entities/UserOrganization.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; +using Aquiis.SimpleStart.Core.Validation; namespace Aquiis.SimpleStart.Core.Entities { @@ -10,10 +11,9 @@ namespace Aquiis.SimpleStart.Core.Entities public class UserOrganization { - [Required] - [StringLength(100)] + [RequiredGuid] [Display(Name = "UserOrganization ID")] - public string Id { get; set; } = Guid.NewGuid().ToString(); + public Guid Id { get; set; } = Guid.NewGuid(); /// /// The user being granted access @@ -23,7 +23,8 @@ public class UserOrganization /// /// The organization they're being granted access to /// - public string OrganizationId { get; set; } = string.Empty; + [RequiredGuid] + public Guid OrganizationId { get; set; } = Guid.Empty; /// /// Role within this organization: "Owner", "Administrator", "PropertyManager", "User" diff --git a/Aquiis.SimpleStart/Core/Validation/OptionalGuidAttribute.cs b/Aquiis.SimpleStart/Core/Validation/OptionalGuidAttribute.cs new file mode 100644 index 0000000..ac45d87 --- /dev/null +++ b/Aquiis.SimpleStart/Core/Validation/OptionalGuidAttribute.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.SimpleStart.Core.Validation; + +/// +/// Validates that an optional Guid property, if provided, is not Guid.Empty. +/// Use this for Guid? properties where null is acceptable but Guid.Empty is not. +/// +/// Example: LeaseId on MaintenanceRequest - can be null (no lease yet) but shouldn't be Guid.Empty (invalid reference) +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +public class OptionalGuidAttribute : ValidationAttribute +{ + /// + /// Initializes a new instance of OptionalGuidAttribute with a default error message. + /// + public OptionalGuidAttribute() + : base("The {0} field cannot be empty if provided. Either leave it null or provide a valid value.") + { + } + + /// + /// Initializes a new instance of OptionalGuidAttribute with a custom error message. + /// + /// The error message to display when validation fails. + public OptionalGuidAttribute(string errorMessage) + : base(errorMessage) + { + } + + /// + /// Validates that if the value is not null, it must not be Guid.Empty. + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + // Null is acceptable for optional fields + if (value == null) + { + return ValidationResult.Success; + } + + // Type check + if (value is not Guid guidValue) + { + return new ValidationResult( + $"The {validationContext.DisplayName} field must be a valid Guid or null.", + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + // Reject Guid.Empty (if you provide a value, it must be real) + if (guidValue == Guid.Empty) + { + return new ValidationResult( + FormatErrorMessage(validationContext.DisplayName), + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + return ValidationResult.Success; + } + + public override bool IsValid(object? value) + { + if (value == null) + return true; + + if (value is not Guid guidValue) + return false; + + return guidValue != Guid.Empty; + } +} diff --git a/Aquiis.SimpleStart/Core/Validation/RequiredGuidAttribute.cs b/Aquiis.SimpleStart/Core/Validation/RequiredGuidAttribute.cs new file mode 100644 index 0000000..ef64f6a --- /dev/null +++ b/Aquiis.SimpleStart/Core/Validation/RequiredGuidAttribute.cs @@ -0,0 +1,83 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.SimpleStart.Core.Validation; + +/// +/// Validates that a Guid property has a value other than Guid.Empty. +/// Use this instead of [Required] for non-nullable Guid properties. +/// +/// Note: For nullable Guid? properties, use [Required] to check for null, +/// and optionally combine with [RequiredGuid] to also reject Guid.Empty. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +public class RequiredGuidAttribute : ValidationAttribute +{ + /// + /// Initializes a new instance of RequiredGuidAttribute with a default error message. + /// + public RequiredGuidAttribute() + : base("The {0} field is required and cannot be empty.") + { + } + + /// + /// Initializes a new instance of RequiredGuidAttribute with a custom error message. + /// + /// The error message to display when validation fails. + public RequiredGuidAttribute(string errorMessage) + : base(errorMessage) + { + } + + /// + /// Validates that the value is not null, not Guid.Empty, and is a valid Guid. + /// + /// The value to validate. + /// The context information about the validation operation. + /// ValidationResult.Success if valid, otherwise a ValidationResult with error message. + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + // Null check (for Guid? properties) + if (value == null) + { + return new ValidationResult( + FormatErrorMessage(validationContext.DisplayName), + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + // Type check + if (value is not Guid guidValue) + { + return new ValidationResult( + $"The {validationContext.DisplayName} field must be a valid Guid.", + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + // Empty Guid check + if (guidValue == Guid.Empty) + { + return new ValidationResult( + FormatErrorMessage(validationContext.DisplayName), + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + return ValidationResult.Success; + } + + /// + /// Simple validation for attribute usage without ValidationContext. + /// + public override bool IsValid(object? value) + { + if (value == null) + return false; + + if (value is not Guid guidValue) + return false; + + return guidValue != Guid.Empty; + } +} diff --git a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor index 598115f..4b98ecb 100644 --- a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor +++ b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor @@ -1,4 +1,4 @@ -@page "/administration/organizations/edit/{Id}" +@page "/administration/organizations/edit/{Id:guid}" @using Aquiis.SimpleStart.Core.Entities @using Aquiis.SimpleStart.Application.Services @@ -149,7 +149,7 @@ @code { [Parameter] - public string Id { get; set; } = string.Empty; + public Guid Id { get; set; } = Guid.Empty; private bool isLoading = true; private bool isSubmitting = false; diff --git a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor index 6cc9caa..8b31ee1 100644 --- a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor +++ b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor @@ -1,4 +1,4 @@ -@page "/administration/organizations/{Id}/users" +@page "/administration/organizations/{Id:guid}/users" @using Aquiis.SimpleStart.Core.Entities @using Aquiis.SimpleStart.Application.Services @@ -215,7 +215,7 @@ @code { [Parameter] - public string Id { get; set; } = string.Empty; + public Guid Id { get; set; } = Guid.Empty; private bool isLoading = true; private Organization? organization; @@ -237,7 +237,7 @@ isLoading = true; try { - var userId = await UserContext.GetUserIdAsync(); + string? userId = await UserContext.GetUserIdAsync(); if (string.IsNullOrEmpty(userId)) { return; diff --git a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor index 4aa69b8..1655a67 100644 --- a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor +++ b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor @@ -197,17 +197,17 @@ Navigation.NavigateTo("/administration/organizations/create"); } - private void NavigateToView(string organizationId) + private void NavigateToView(Guid organizationId) { Navigation.NavigateTo($"/administration/organizations/view/{organizationId}"); } - private void NavigateToEdit(string organizationId) + private void NavigateToEdit(Guid organizationId) { Navigation.NavigateTo($"/administration/organizations/edit/{organizationId}"); } - private void NavigateToManageUsers(string organizationId) + private void NavigateToManageUsers(Guid organizationId) { Navigation.NavigateTo($"/administration/organizations/{organizationId}/users"); } diff --git a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor index 2e2c0b0..0fceb30 100644 --- a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor +++ b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor @@ -1,4 +1,4 @@ -@page "/administration/organizations/view/{Id}" +@page "/administration/organizations/view/{Id:guid}" @using Aquiis.SimpleStart.Core.Entities @using Aquiis.SimpleStart.Application.Services @@ -225,7 +225,7 @@ @code { [Parameter] - public string Id { get; set; } = string.Empty; + public Guid Id { get; set; } = Guid.Empty; private bool isLoading = true; private Organization? organization; diff --git a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor index db46c1b..58da31c 100644 --- a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor +++ b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor @@ -275,8 +275,8 @@ else protected override async Task OnInitializedAsync() { // Get organization and role context - var org = await UserContext.GetActiveOrganizationAsync(); - organizationName = org?.Name ?? "Unknown Organization"; + var organization = await UserContext.GetActiveOrganizationAsync(); + organizationName = organization?.Name ?? "Unknown Organization"; userRole = await UserContext.GetCurrentOrganizationRoleAsync() ?? "User"; canEdit = userRole != "User"; // User role is read-only @@ -288,10 +288,10 @@ else loading = true; try { - var orgId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(orgId)) + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) { - settings = await SettingsService.GetSettingsAsync(orgId); + settings = await SettingsService.GetSettingsAsync(organizationId.Value); } } catch (Exception ex) diff --git a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor index d473b39..72cf23f 100644 --- a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor +++ b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor @@ -369,7 +369,7 @@ private bool isSubmitting = false; private string errorMessage = string.Empty; private string successMessage = string.Empty; - private string organizationId = string.Empty; + private Guid organizationId = Guid.Empty; private string organizationName = string.Empty; private string userRole = string.Empty; private bool canEdit = true; @@ -379,7 +379,7 @@ { try { - organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? string.Empty; + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; await LoadSettings(); } catch (Exception ex) @@ -426,6 +426,7 @@ { settings = new OrganizationSettingsEntity { + Id = Guid.NewGuid(), OrganizationId = organizationId, Name = "My Organization", ApplicationFeeEnabled = true, diff --git a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor index 97f517e..0e2cb7e 100644 --- a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor +++ b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor @@ -205,8 +205,8 @@ runningTask = taskType; taskResults.Clear(); - var organizationId = await GetActiveOrganizationIdAsync(); - if (organizationId == null) + Guid? organizationId = await GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue) { ToastService.ShowError("Could not determine organization ID"); return; @@ -215,16 +215,16 @@ switch (taskType) { case TaskType.ApplyLateFees: - await ApplyLateFees(organizationId); + await ApplyLateFees(organizationId.Value); break; case TaskType.UpdateInvoiceStatuses: - await UpdateInvoiceStatuses(organizationId); + await UpdateInvoiceStatuses(organizationId.Value); break; case TaskType.SendPaymentReminders: - await SendPaymentReminders(organizationId); + await SendPaymentReminders(organizationId.Value); break; case TaskType.CheckLeaseRenewals: - await CheckLeaseRenewals(organizationId); + await CheckLeaseRenewals(organizationId.Value); break; } @@ -258,10 +258,10 @@ return; } - await ApplyLateFees(organizationId); - await UpdateInvoiceStatuses(organizationId); - await SendPaymentReminders(organizationId); - await CheckLeaseRenewals(organizationId); + await ApplyLateFees(organizationId.Value); + await UpdateInvoiceStatuses(organizationId.Value); + await SendPaymentReminders(organizationId.Value); + await CheckLeaseRenewals(organizationId.Value); lastRunTime = DateTime.Now; ToastService.ShowSuccess("All tasks completed successfully"); @@ -278,13 +278,13 @@ } } - private async Task GetActiveOrganizationIdAsync() + private async Task GetActiveOrganizationIdAsync() { // Get organization ID from UserContext return await UserContext.GetActiveOrganizationIdAsync(); } - private async Task ApplyLateFees(string organizationId) + private async Task ApplyLateFees(Guid organizationId) { var settings = await PropertyService.GetOrganizationSettingsByOrgIdAsync(organizationId); @@ -330,7 +330,7 @@ taskResults[TaskType.ApplyLateFees] = $"Applied late fees to {overdueInvoices.Count} invoice(s)"; } - private async Task UpdateInvoiceStatuses(string organizationId) + private async Task UpdateInvoiceStatuses(Guid organizationId) { var today = DateTime.Today; var newlyOverdueInvoices = await DbContext.Invoices @@ -356,7 +356,7 @@ taskResults[TaskType.UpdateInvoiceStatuses] = $"Updated {newlyOverdueInvoices.Count} invoice(s) to Overdue status"; } - private async Task SendPaymentReminders(string organizationId) + private async Task SendPaymentReminders(Guid organizationId) { var settings = await PropertyService.GetOrganizationSettingsByOrgIdAsync(organizationId); if (settings == null || !settings.PaymentReminderEnabled) @@ -396,7 +396,7 @@ taskResults[TaskType.SendPaymentReminders] = $"Marked {upcomingInvoices.Count} invoice(s) for payment reminders"; } - private async Task CheckLeaseRenewals(string organizationId) + private async Task CheckLeaseRenewals(Guid organizationId) { var today = DateTime.Today; int totalProcessed = 0; diff --git a/Aquiis.SimpleStart/Features/Administration/Users/Manage.razor b/Aquiis.SimpleStart/Features/Administration/Users/Manage.razor index b907bf9..6506adb 100644 --- a/Aquiis.SimpleStart/Features/Administration/Users/Manage.razor +++ b/Aquiis.SimpleStart/Features/Administration/Users/Manage.razor @@ -377,7 +377,7 @@ else private UserInfo? selectedUserForEdit; private RoleEditModel roleEditModel = new(); - private string organizationId = string.Empty; + private Guid organizationId = Guid.Empty; private string organizationName = string.Empty; @@ -386,7 +386,7 @@ else try { // One line instead of 10+! - organizationId = await UserContext!.GetActiveOrganizationIdAsync() ?? string.Empty; + organizationId = await UserContext!.GetActiveOrganizationIdAsync() ?? Guid.Empty; organizationName = (await UserContext!.GetOrganizationByIdAsync(organizationId))?.Name ?? string.Empty; await LoadData(); } diff --git a/Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor b/Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor index ec0233f..20724ac 100644 --- a/Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor +++ b/Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor @@ -200,7 +200,7 @@ var currentUserId = await UserContext.GetUserIdAsync(); var currentOrganizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (string.IsNullOrEmpty(currentUserId) || string.IsNullOrEmpty(currentOrganizationId)) + if (string.IsNullOrEmpty(currentUserId) || !currentOrganizationId.HasValue) { errorMessage = "User not authenticated or no active organization."; return; @@ -223,8 +223,8 @@ PhoneNumber = userModel.PhoneNumber, FirstName = userModel.FirstName, LastName = userModel.LastName, - OrganizationId = currentOrganizationId, - ActiveOrganizationId = currentOrganizationId + OrganizationId = currentOrganizationId.Value, + ActiveOrganizationId = currentOrganizationId.Value }; var createResult = await UserManager.CreateAsync(newUser, userModel.Password); @@ -238,7 +238,7 @@ // Grant organization access with the selected role var grantResult = await OrganizationService.GrantOrganizationAccessAsync( newUser.Id, - currentOrganizationId, + currentOrganizationId.Value, userModel.SelectedRole, currentUserId); diff --git a/Aquiis.SimpleStart/Features/Administration/Users/View.razor b/Aquiis.SimpleStart/Features/Administration/Users/View.razor index 1b55f41..78c01b6 100644 --- a/Aquiis.SimpleStart/Features/Administration/Users/View.razor +++ b/Aquiis.SimpleStart/Features/Administration/Users/View.razor @@ -326,7 +326,7 @@ else // Check permissions var currentOrgId = await UserContext.GetActiveOrganizationIdAsync(); - var currentUserRole = await OrganizationService.GetUserRoleForOrganizationAsync(currentUser.Id, currentOrgId ?? string.Empty); + var currentUserRole = await OrganizationService.GetUserRoleForOrganizationAsync(currentUser.Id, currentOrgId.Value); isCurrentUserAdmin = currentUserRole == ApplicationConstants.OrganizationRoles.Owner || currentUserRole == ApplicationConstants.OrganizationRoles.Administrator; isViewingOwnAccount = currentUser.Id == viewedUser.Id; @@ -339,7 +339,7 @@ else canEditAccount = isViewingOwnAccount; // Load user's organization role - var viewedUserRole = await OrganizationService.GetUserRoleForOrganizationAsync(viewedUser.Id, currentOrgId ?? string.Empty); + var viewedUserRole = await OrganizationService.GetUserRoleForOrganizationAsync(viewedUser.Id, currentOrgId.Value); userRoles = viewedUserRole != null ? new List { viewedUserRole } : new List(); currentUserRole = viewedUserRole ?? "No Role"; selectedRole = currentUserRole; @@ -394,14 +394,14 @@ else var currentOrgId = await UserContext.GetActiveOrganizationIdAsync(); var currentUserId = await UserContext.GetUserIdAsync(); - if (string.IsNullOrEmpty(currentOrgId) || string.IsNullOrEmpty(currentUserId)) + if (!currentOrgId.HasValue || string.IsNullOrEmpty(currentUserId)) { errorMessage = "Unable to determine current organization context."; return; } // Check if user already has an organization assignment - var existingRole = await OrganizationService.GetUserRoleForOrganizationAsync(viewedUser.Id, currentOrgId); + var existingRole = await OrganizationService.GetUserRoleForOrganizationAsync(viewedUser.Id, currentOrgId.Value); bool updateResult; if (existingRole != null) @@ -409,7 +409,7 @@ else // Update existing role updateResult = await OrganizationService.UpdateUserRoleAsync( viewedUser.Id, - currentOrgId, + currentOrgId.Value, selectedRole, currentUserId); } @@ -418,7 +418,7 @@ else // Grant new organization access with the selected role updateResult = await OrganizationService.GrantOrganizationAccessAsync( viewedUser.Id, - currentOrgId, + currentOrgId.Value, selectedRole, currentUserId); } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Applications.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Applications.razor index f6fa877..e76879a 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Applications.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Applications.razor @@ -187,13 +187,13 @@ private List deniedApplications = new(); private string currentFilter = "Pending"; private bool isLoading = true; - private string organizationId = string.Empty; + private Guid organizationId = Guid.Empty; protected override async Task OnInitializedAsync() { try { - organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? string.Empty; + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; await LoadApplications(); } catch (Exception ex) @@ -241,7 +241,7 @@ }; } - private void ViewApplication(int applicationId) + private void ViewApplication(Guid applicationId) { Navigation.NavigateTo($"/propertymanagement/applications/{applicationId}/review"); } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor index 4facace..0379e99 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor @@ -1,14 +1,16 @@ -@page "/propertymanagement/applications/{ApplicationId:int}/generate-lease-offer" +@page "/propertymanagement/applications/{ApplicationId:guid}/generate-lease-offer" @using Aquiis.SimpleStart.Core.Entities @using Aquiis.SimpleStart.Application.Services @using Aquiis.SimpleStart.Shared.Services @using Aquiis.SimpleStart.Application.Services.PdfGenerators +@using Aquiis.SimpleStart.Application.Services.Workflows @using Aquiis.SimpleStart.Core.Constants @using Microsoft.AspNetCore.Authorization @using System.ComponentModel.DataAnnotations @inject PropertyManagementService PropertyService +@inject ApplicationWorkflowService WorkflowService @inject NavigationManager Navigation @inject AuthenticationStateProvider AuthStateProvider @inject UserContextService UserContext @@ -259,7 +261,7 @@ @code { [Parameter] - public int ApplicationId { get; set; } + public Guid ApplicationId { get; set; } private RentalApplication? application; private LeaseOfferModel leaseModel = new(); @@ -267,14 +269,14 @@ private bool isSubmitting = false; private string errorMessage = string.Empty; private string userId = string.Empty; - private string organizationId = string.Empty; + private Guid organizationId = Guid.Empty; protected override async Task OnInitializedAsync() { try { userId = await UserContext.GetUserIdAsync() ?? string.Empty; - organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? string.Empty; + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; await LoadApplication(); } @@ -326,58 +328,28 @@ return; } - // Create the lease offer - var leaseOffer = new LeaseOffer + // Use workflow service to generate lease offer + var offerModel = new Aquiis.SimpleStart.Application.Services.Workflows.LeaseOfferModel { - OrganizationId = organizationId, - RentalApplicationId = application.Id, - PropertyId = application.PropertyId, - ProspectiveTenantId = application.ProspectiveTenantId, StartDate = leaseModel.StartDate, EndDate = leaseModel.EndDate, MonthlyRent = leaseModel.MonthlyRent, SecurityDeposit = leaseModel.SecurityDeposit, Terms = leaseModel.Terms, - Notes = leaseModel.Notes ?? string.Empty, - Status = "Pending", - OfferedOn = DateTime.UtcNow, - ExpiresOn = DateTime.UtcNow.AddDays(30), // 30-day expiration - CreatedOn = DateTime.UtcNow, - CreatedBy = userId + Notes = leaseModel.Notes }; - var createdOffer = await PropertyService.CreateLeaseOfferAsync(leaseOffer); + var result = await WorkflowService.GenerateLeaseOfferAsync(application.Id, offerModel); - if (createdOffer == null) + if (result.Success) { - errorMessage = "Failed to create lease offer."; - return; - } - - // Update property status to LeasePending - var property = await PropertyService.GetPropertyByIdAsync(application.PropertyId); - if (property != null) - { - property.Status = ApplicationConstants.PropertyStatuses.LeasePending; - await PropertyService.UpdatePropertyAsync(property); + ToastService.ShowSuccess("Lease offer generated successfully!"); + Navigation.NavigateTo($"/propertymanagement/leaseoffers/view/{result.Data!.Id}"); } - - // Update application status - application.Status = "LeaseOffered"; // We'll need to add this status - await PropertyService.UpdateRentalApplicationAsync(application); - - // Update prospect status - if (application.ProspectiveTenant != null) + else { - application.ProspectiveTenant.Status = "LeaseOffered"; // We'll need to add this status - await PropertyService.UpdateProspectiveTenantAsync(application.ProspectiveTenant); + errorMessage = string.Join(", ", result.Errors); } - - // Auto-deny all other pending applications for this property - await DenyCompetingApplications(application.PropertyId, application.Id); - - ToastService.ShowSuccess("Lease offer generated successfully!"); - Navigation.NavigateTo($"/propertymanagement/leaseoffers/view/{createdOffer.Id}"); } catch (Exception ex) { @@ -391,28 +363,8 @@ private async Task DenyCompetingApplications(int propertyId, int currentApplicationId) { - try - { - var allApplications = await PropertyService.GetAllRentalApplicationsAsync(); - var competingApps = allApplications.Where(a => - a.PropertyId == propertyId && - a.Id != currentApplicationId && - (a.Status == ApplicationConstants.ApplicationStatuses.Submitted || - a.Status == ApplicationConstants.ApplicationStatuses.UnderReview || - a.Status == ApplicationConstants.ApplicationStatuses.Screening)).ToList(); - - foreach (var app in competingApps) - { - await PropertyService.DenyApplicationAsync( - app.Id, - "Property is no longer available - lease offered to another applicant"); - } - } - catch (Exception ex) - { - // Log but don't fail the main operation - Console.WriteLine($"Error denying competing applications: {ex.Message}"); - } + // This method is no longer needed - workflow service handles it automatically + await Task.CompletedTask; } private string GetDefaultLeaseTerms() diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor index 799ecf1..5a92252 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor @@ -290,7 +290,7 @@ try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue && organizationId != Guid.Empty) { prospects = await PropertyService.GetAllProspectiveTenantsAsync(); @@ -300,6 +300,10 @@ var allProperties = await PropertyService.GetPropertiesAsync(); properties = allProperties.Where(p => p.IsAvailable).ToList(); } + else + { + ToastService.ShowError("Organization context not available"); + } } catch (Exception ex) { @@ -330,7 +334,7 @@ var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (string.IsNullOrEmpty(organizationId) || string.IsNullOrEmpty(userId)) + if (!organizationId.HasValue || organizationId == Guid.Empty || string.IsNullOrEmpty(userId)) { ToastService.ShowError("User context not available"); return; @@ -350,7 +354,7 @@ Notes = newProspect.Notes, InterestedPropertyId = newProspect.InterestedPropertyId, DesiredMoveInDate = newProspect.DesiredMoveInDate, - OrganizationId = organizationId, + OrganizationId = organizationId.Value, }; await PropertyService.CreateProspectiveTenantAsync(prospect); @@ -370,22 +374,22 @@ filterStatus = status; } - private void ScheduleTour(int prospectId) + private void ScheduleTour(Guid prospectId) { Navigation.NavigateTo($"/PropertyManagement/Tours/Schedule/{prospectId}"); } - private void BeginApplication(int prospectId) + private void BeginApplication(Guid prospectId) { Navigation.NavigateTo($"/propertymanagement/prospects/{prospectId}/submit-application"); } - private void ViewDetails(int prospectId) + private void ViewDetails(Guid prospectId) { Navigation.NavigateTo($"/PropertyManagement/ProspectiveTenants/{prospectId}"); } - private async Task DeleteProspect(int prospectId) + private async Task DeleteProspect(Guid prospectId) { // TODO: Add confirmation dialog in future sprint try @@ -393,12 +397,16 @@ var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && organizationId != Guid.Empty && !string.IsNullOrEmpty(userId)) { await PropertyService.DeleteProspectiveTenantAsync(prospectId); ToastService.ShowSuccess("Prospect deleted successfully"); await LoadData(); } + else + { + ToastService.ShowError("User context not available"); + } } catch (Exception ex) { @@ -458,7 +466,7 @@ [StringLength(2000)] public string? Notes { get; set; } - public int? InterestedPropertyId { get; set; } + public Guid? InterestedPropertyId { get; set; } public DateTime? DesiredMoveInDate { get; set; } } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor index 8eb4bcc..ae1e428 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor @@ -1,14 +1,16 @@ -@page "/propertymanagement/applications/{ApplicationId:int}/review" +@page "/propertymanagement/applications/{ApplicationId:guid}/review" @using Aquiis.SimpleStart.Core.Entities @using Aquiis.SimpleStart.Application.Services @using Aquiis.SimpleStart.Shared.Services @using Aquiis.SimpleStart.Application.Services.PdfGenerators +@using Aquiis.SimpleStart.Application.Services.Workflows @using Aquiis.SimpleStart.Core.Constants @using Microsoft.AspNetCore.Authorization @using System.ComponentModel.DataAnnotations @inject PropertyManagementService PropertyService +@inject ApplicationWorkflowService WorkflowService @inject NavigationManager Navigation @inject AuthenticationStateProvider AuthStateProvider @inject UserContextService UserContext @@ -399,7 +401,9 @@ Back
- @if (application.Status == ApplicationConstants.ApplicationStatuses.Submitted && screening == null) + @if ((application.Status == ApplicationConstants.ApplicationStatuses.Submitted || + application.Status == ApplicationConstants.ApplicationStatuses.UnderReview) && + screening == null && application.ApplicationFeePaid) {
} - + +
Applicant Information
@@ -102,7 +106,7 @@
Property Selection
- + @foreach (var property in availableProperties) { @@ -359,7 +363,7 @@ @code { [Parameter] - public int ProspectId { get; set; } + public Guid ProspectId { get; set; } private ProspectiveTenant? prospect; private RentalApplication? existingApplication; @@ -372,7 +376,7 @@ private decimal applicationFee = 50.00m; private string errorMessage = string.Empty; private string userId = string.Empty; - private string organizationId = string.Empty; + private Guid organizationId = Guid.Empty; protected override async Task OnInitializedAsync() { @@ -380,7 +384,7 @@ { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? string.Empty; - organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? string.Empty; + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; await LoadData(); } @@ -426,37 +430,55 @@ } } + private void UpdateSelectedProperty() + { + if (applicationModel.PropertyId != Guid.Empty) + { + selectedProperty = availableProperties.FirstOrDefault(p => p.Id == applicationModel.PropertyId); + } + else + { + selectedProperty = null; + } + } + private async Task OnPropertyChanged(ChangeEventArgs e) { - if (int.TryParse(e.Value?.ToString(), out int propertyId) && propertyId > 0) + if (Guid.TryParse(e.Value?.ToString(), out Guid propertyId) && propertyId != Guid.Empty) { + applicationModel.PropertyId = propertyId; selectedProperty = availableProperties.FirstOrDefault(p => p.Id == propertyId); } else { + applicationModel.PropertyId = Guid.Empty; selectedProperty = null; } StateHasChanged(); await Task.CompletedTask; - } private async Task HandleSubmitApplication() { - if (prospect == null || selectedProperty == null) return; + Console.WriteLine("HandleSubmitApplication called"); + + if (prospect == null || selectedProperty == null) + { + errorMessage = prospect == null ? "Prospect not found" : "Please select a property"; + Console.WriteLine($"Validation failed: {errorMessage}"); + return; + } isSubmitting = true; errorMessage = string.Empty; try { - var application = new RentalApplication + Console.WriteLine($"Submitting application for prospect {ProspectId}, property {applicationModel.PropertyId}"); + + // Use ApplicationWorkflowService to submit application + var submissionModel = new Aquiis.SimpleStart.Application.Services.Workflows.ApplicationSubmissionModel { - ProspectiveTenantId = ProspectId, - PropertyId = applicationModel.PropertyId, - AppliedOn = DateTime.UtcNow, - Status = ApplicationConstants.ApplicationStatuses.Submitted, - // Current Address CurrentAddress = applicationModel.CurrentAddress, CurrentCity = applicationModel.CurrentCity, @@ -482,20 +504,30 @@ // Fees ApplicationFee = applicationFee, - ApplicationFeePaid = false, // Will be paid separately - - OrganizationId = organizationId - + ApplicationFeePaid = false // Will be paid separately }; - var createdApplication = await PropertyService.CreateRentalApplicationAsync(application); + var result = await WorkflowService.SubmitApplicationAsync( + ProspectId, + applicationModel.PropertyId, + submissionModel); + + Console.WriteLine($"Workflow result: Success={result.Success}, Errors={string.Join(", ", result.Errors)}"); - ToastService.ShowSuccess("Application submitted successfully! You will be notified once reviewed."); - Navigation.NavigateTo($"/PropertyManagement/ProspectiveTenants/{ProspectId}"); + if (result.Success) + { + ToastService.ShowSuccess("Application submitted successfully! You will be notified once reviewed."); + Navigation.NavigateTo($"/PropertyManagement/ProspectiveTenants/{ProspectId}"); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } } catch (Exception ex) { errorMessage = $"Error submitting application: {ex.Message}"; + Console.WriteLine($"Exception in HandleSubmitApplication: {ex}"); } finally { @@ -510,9 +542,8 @@ public class ApplicationSubmissionModel { - [Required(ErrorMessage = "Please select a property")] - [Range(1, int.MaxValue, ErrorMessage = "Please select a property")] - public int PropertyId { get; set; } + [RequiredGuid(ErrorMessage = "Please select a property")] + public Guid PropertyId { get; set; } [Required] [StringLength(200)] diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Tours.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Tours.razor index 1cf8091..96abbee 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Tours.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Tours.razor @@ -272,7 +272,7 @@ try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { allTours = await PropertyService.GetAllToursAsync(); upcomingTours = await PropertyService.GetUpcomingToursAsync(7); @@ -303,12 +303,12 @@ Navigation.NavigateTo("/PropertyManagement/Tours/Calendar"); } - private async Task MarkCompleted(int tourId) + private async Task MarkCompleted(Guid tourId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { var tour = await PropertyService.GetTourByIdAsync(tourId); if (tour != null) @@ -331,14 +331,14 @@ } } - private async Task CancelTour(int tourId) + private async Task CancelTour(Guid tourId) { // TODO: Add confirmation dialog in future sprint try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { await PropertyService.CancelTourAsync(tourId); @@ -352,7 +352,7 @@ } } - private void ViewFeedback(int showingId) + private void ViewFeedback(Guid showingId) { Navigation.NavigateTo($"/PropertyManagement/Showings/Feedback/{showingId}"); } @@ -390,7 +390,7 @@ _ => "bg-secondary" }; - private void ViewTourChecklist(int checklistId) + private void ViewTourChecklist(Guid checklistId) { Navigation.NavigateTo($"/PropertyManagement/Checklists/View/{checklistId}"); } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor index 1a6f6d9..14eebed 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor @@ -207,7 +207,7 @@ try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { allTours = await PropertyService.GetAllToursAsync(); } @@ -562,12 +562,12 @@ selectedTour = null; } - private async Task MarkCompleted(int tourId) + private async Task MarkCompleted(Guid tourId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { var tour = await PropertyService.GetTourByIdAsync(tourId); if (tour != null) @@ -590,14 +590,14 @@ } } - private async Task CancelTour(int tourId) + private async Task CancelTour(Guid tourId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { await PropertyService.CancelTourAsync(tourId); ToastService.ShowSuccess("Tour cancelled successfully"); diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor index a023e39..1f89a0b 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor @@ -1,4 +1,4 @@ -@page "/PropertyManagement/ProspectiveTenants/{ProspectId:int}" +@page "/PropertyManagement/ProspectiveTenants/{ProspectId:guid}" @using Aquiis.SimpleStart.Core.Entities @using Aquiis.SimpleStart.Application.Services @@ -568,7 +568,7 @@ @code { [Parameter] - public int ProspectId { get; set; } + public Guid ProspectId { get; set; } private ProspectiveTenant? prospect; private List tours = new(); @@ -588,8 +588,8 @@ loading = true; try { - var orgId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(orgId)) + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) { prospect = await PropertyService.GetProspectiveTenantByIdAsync(ProspectId); @@ -659,7 +659,7 @@ prospect.IdentificationState = editModel.IdentificationState; prospect.Source = editModel.Source; prospect.Notes = editModel.Notes; - prospect.InterestedPropertyId = int.TryParse(editModel.InterestedPropertyId, out var propId) && propId > 0 ? propId : null; + prospect.InterestedPropertyId = Guid.TryParse(editModel.InterestedPropertyId, out var propId) && propId != Guid.Empty ? propId : null; prospect.DesiredMoveInDate = editModel.DesiredMoveInDate; await PropertyService.UpdateProspectiveTenantAsync(prospect); diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Calendar.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Calendar.razor index c86406e..48c6cdc 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Calendar.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Calendar.razor @@ -643,9 +643,9 @@ { // Load filter defaults from settings var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { - var settings = await CalendarSettingsService.GetSettingsAsync(organizationId); + var settings = await CalendarSettingsService.GetSettingsAsync(organizationId.Value); selectedEventTypes = settings .Where(s => s.ShowOnCalendar) .Select(s => s.EntityType) @@ -667,7 +667,7 @@ try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { // Get date range based on current view var (startDate, endDate) = viewMode switch @@ -741,7 +741,7 @@ Icon = "bi-calendar-event", EventType = CalendarEventTypes.Custom, Status = "Scheduled", - OrganizationId = !string.IsNullOrEmpty(organizationId) ? organizationId : string.Empty, + OrganizationId = organizationId.HasValue ? organizationId.Value : Guid.Empty, CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, CreatedOn = DateTime.UtcNow }; @@ -1211,7 +1211,7 @@ try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (string.IsNullOrEmpty(organizationId)) return; + if (!organizationId.HasValue) return; switch (calendarEvent.EventType) { @@ -1248,7 +1248,7 @@ } } - private async Task ShowTourDetailById(int tourId) + private async Task ShowTourDetailById(Guid tourId) { var tour = await PropertyService.GetTourByIdAsync(tourId); if (tour != null) @@ -1261,7 +1261,7 @@ } } - private async Task ShowInspectionDetailById(int inspectionId) + private async Task ShowInspectionDetailById(Guid inspectionId) { var inspection = await PropertyService.GetInspectionByIdAsync(inspectionId); if (inspection != null) @@ -1274,7 +1274,7 @@ } } - private async Task ShowMaintenanceRequestDetailById(int maintenanceId) + private async Task ShowMaintenanceRequestDetailById(Guid maintenanceId) { var maintenanceRequest = await PropertyService.GetMaintenanceRequestByIdAsync(maintenanceId); if (maintenanceRequest != null) @@ -1336,12 +1336,12 @@ Navigation.NavigateTo($"/PropertyManagement/Properties/View/{property.Id}"); } - private async Task CompleteTour(int tourId) + private async Task CompleteTour(Guid tourId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { var tour = await PropertyService.GetTourByIdAsync(tourId); if (tour != null) @@ -1364,14 +1364,14 @@ } } - private async Task CancelTour(int tourId) + private async Task CancelTour(Guid tourId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { await PropertyService.CancelTourAsync(tourId); ToastService.ShowSuccess("Tour cancelled successfully"); @@ -1385,14 +1385,14 @@ } } - private async Task MarkTourAsNoShow(int tourId) + private async Task MarkTourAsNoShow(Guid tourId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { await PropertyService.MarkTourAsNoShowAsync(tourId); ToastService.ShowSuccess("Tour marked as No Show"); @@ -1406,14 +1406,14 @@ } } - private async Task StartWork(int maintenanceRequestId) + private async Task StartWork(Guid maintenanceRequestId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { var request = await PropertyService.GetMaintenanceRequestByIdAsync(maintenanceRequestId); if (request != null) @@ -1436,14 +1436,14 @@ } } - private async Task CompleteMaintenanceRequest(int maintenanceRequestId) + private async Task CompleteMaintenanceRequest(Guid maintenanceRequestId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { var request = await PropertyService.GetMaintenanceRequestByIdAsync(maintenanceRequestId); if (request != null) @@ -1465,14 +1465,14 @@ } } - private async Task CancelMaintenanceRequest(int maintenanceRequestId) + private async Task CancelMaintenanceRequest(Guid maintenanceRequestId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { var request = await PropertyService.GetMaintenanceRequestByIdAsync(maintenanceRequestId); if (request != null) @@ -1492,14 +1492,14 @@ } } - private async Task UpdateCustomEventStatus(int eventId, string newStatus) + private async Task UpdateCustomEventStatus(Guid eventId, string newStatus) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (string.IsNullOrEmpty(organizationId) || string.IsNullOrEmpty(userId)) + if (!organizationId.HasValue || string.IsNullOrEmpty(userId)) { ToastService.ShowError("Unable to identify user or organization"); return; @@ -1542,7 +1542,7 @@ Navigation.NavigateTo("/PropertyManagement/Calendar/ListView"); } - private void CompleteRoutineInspection(int propertyId) + private void CompleteRoutineInspection(Guid propertyId) { // Navigate to create new inspection form with the property pre-selected Navigation.NavigateTo($"/PropertyManagement/Inspections/Create?propertyId={propertyId}"); diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/CalendarListView.razor b/Aquiis.SimpleStart/Features/PropertyManagement/CalendarListView.razor index 32cb8b8..b5e66d7 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/CalendarListView.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/CalendarListView.razor @@ -542,7 +542,7 @@ Navigation.NavigateTo("/PropertyManagement/Calendar"); } - private void CompleteRoutineInspection(int propertyId) + private void CompleteRoutineInspection(Guid propertyId) { // Navigate to create new inspection form with the property pre-selected Navigation.NavigateTo($"/PropertyManagement/Inspections/Create?propertyId={propertyId}"); @@ -562,7 +562,7 @@ try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (string.IsNullOrEmpty(organizationId)) return; + if (!organizationId.HasValue) return; switch (calendarEvent.EventType) { @@ -599,7 +599,7 @@ } } - private async Task ShowTourDetailById(int tourId) + private async Task ShowTourDetailById(Guid tourId) { var tour = await PropertyService.GetTourByIdAsync(tourId); if (tour != null) @@ -612,7 +612,7 @@ } } - private async Task ShowInspectionDetailById(int inspectionId) + private async Task ShowInspectionDetailById(Guid inspectionId) { var inspection = await PropertyService.GetInspectionByIdAsync(inspectionId); if (inspection != null) @@ -625,7 +625,7 @@ } } - private async Task ShowMaintenanceRequestDetailById(int maintenanceId) + private async Task ShowMaintenanceRequestDetailById(Guid maintenanceId) { var maintenanceRequest = await PropertyService.GetMaintenanceRequestByIdAsync(maintenanceId); if (maintenanceRequest != null) @@ -684,12 +684,12 @@ } } - private async Task CompleteTour(int tourId) + private async Task CompleteTour(Guid tourId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { var tour = await PropertyService.GetTourByIdAsync(tourId); if (tour != null) @@ -712,14 +712,14 @@ } } - private async Task CancelTour(int tourId) + private async Task CancelTour(Guid tourId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { await PropertyService.CancelTourAsync(tourId); ToastService.ShowSuccess("Tour cancelled successfully"); @@ -733,14 +733,14 @@ } } - private async Task MarkTourAsNoShow(int tourId) + private async Task MarkTourAsNoShow(Guid tourId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { await PropertyService.MarkTourAsNoShowAsync(tourId); ToastService.ShowSuccess("Tour marked as No Show"); @@ -754,7 +754,7 @@ } } - private async Task StartWork(int maintenanceRequestId) + private async Task StartWork(Guid maintenanceRequestId) { try { @@ -776,14 +776,14 @@ } } - private async Task CompleteMaintenanceRequest(int maintenanceRequestId) + private async Task CompleteMaintenanceRequest(Guid maintenanceRequestId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { var request = await PropertyService.GetMaintenanceRequestByIdAsync(maintenanceRequestId); if (request != null) @@ -804,14 +804,14 @@ } } - private async Task CancelMaintenanceRequest(int maintenanceRequestId) + private async Task CancelMaintenanceRequest(Guid maintenanceRequestId) { try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - if (!string.IsNullOrEmpty(organizationId) && !string.IsNullOrEmpty(userId)) + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) { var request = await PropertyService.GetMaintenanceRequestByIdAsync(maintenanceRequestId); if (request != null) diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor index 4a26d16..9a27bee 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor @@ -158,7 +158,7 @@ } } - private void StartChecklist(int templateId) + private void StartChecklist(Guid templateId) { // Navigate to complete page with template ID - checklist will be created on save NavigationManager.NavigateTo($"/propertymanagement/checklists/complete/new?templateId={templateId}"); diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor index b4bd97c..2821e65 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor @@ -1,4 +1,4 @@ -@page "/propertymanagement/checklists/complete/{ChecklistId:int}" +@page "/propertymanagement/checklists/complete/{ChecklistId:guid}" @page "/propertymanagement/checklists/complete/new" @using Aquiis.SimpleStart.Core.Entities @@ -10,6 +10,7 @@ @inject ChecklistService ChecklistService @inject PropertyManagementService PropertyManagementService @inject UserContextService UserContext +@inject ToastService ToastService @inject NavigationManager NavigationManager @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer @@ -85,13 +86,13 @@ else } - @if (requiresLease && selectedLeaseId == 0) + @if (requiresLease && selectedLeaseId == Guid.Empty) { This checklist type requires a lease selection }
- @@ -223,7 +224,6 @@ else } - }
@@ -328,10 +328,10 @@ else @code { [Parameter] - public int ChecklistId { get; set; } + public Guid ChecklistId { get; set; } [SupplyParameterFromQuery(Name = "templateId")] - public int? TemplateId { get; set; } + public Guid? TemplateId { get; set; } private Checklist? checklist; private ChecklistTemplate? template; @@ -339,24 +339,24 @@ else private List properties = new(); private List leases = new(); private List checklistItems = new(); - private int selectedPropertyId = 0; - private int selectedLeaseId = 0; + private Guid selectedPropertyId = Guid.Empty; + private Guid selectedLeaseId = Guid.Empty; private bool requiresLease = false; private string? successMessage; private string? errorMessage; private bool isSaving = false; - private Dictionary modifiedItems = new(); + private Dictionary modifiedItems = new(); protected override async Task OnInitializedAsync() { await LoadProperties(); - if (TemplateId.HasValue && TemplateId.Value > 0) + if (TemplateId.HasValue) { // New checklist from template await LoadTemplateForNewChecklist(); } - else if (ChecklistId > 0) + else if (ChecklistId != Guid.Empty) { // Existing checklist await LoadChecklist(); @@ -382,11 +382,12 @@ else // Create a temporary checklist object (not saved to DB yet) checklist = new Checklist { + Id = Guid.NewGuid(), Name = template.Name, ChecklistType = template.Category, ChecklistTemplateId = template.Id, Status = ApplicationConstants.ChecklistStatuses.Draft, - OrganizationId = organizationId!, + OrganizationId = organizationId!.Value, CreatedBy = userId!, CreatedOn = DateTime.UtcNow }; @@ -394,13 +395,14 @@ else // Copy template items to working list checklistItems = template.Items.Select(ti => new ChecklistItem { + Id = Guid.NewGuid(), ItemText = ti.ItemText, ItemOrder = ti.ItemOrder, CategorySection = ti.CategorySection, SectionOrder = ti.SectionOrder, RequiresValue = ti.RequiresValue, IsChecked = false, - OrganizationId = organizationId! + OrganizationId = organizationId!.Value, }).ToList(); // Set Items collection for display @@ -457,14 +459,14 @@ else private async Task OnPropertyChanged() { - if (selectedPropertyId > 0) + if (selectedPropertyId != Guid.Empty) { leases = await PropertyManagementService.GetActiveLeasesByPropertyIdAsync(selectedPropertyId); } else { leases.Clear(); - selectedLeaseId = 0; + selectedLeaseId = Guid.Empty; } } @@ -477,12 +479,31 @@ else isSaving = true; errorMessage = null; - checklist.PropertyId = selectedPropertyId; - checklist.LeaseId = selectedLeaseId > 0 ? selectedLeaseId : null; + checklist.PropertyId = selectedPropertyId != Guid.Empty ? selectedPropertyId : null; + checklist.LeaseId = selectedLeaseId != Guid.Empty ? selectedLeaseId : null; + + if (isNewChecklist) + { + // Create the checklist and persist items + var savedChecklist = await ChecklistService.AddChecklistAsync(checklist); + + // Add any in-memory items to the database + foreach (var item in checklistItems) + { + item.ChecklistId = savedChecklist.Id; + await ChecklistService.AddChecklistItemAsync(item); + } + + ChecklistId = savedChecklist.Id; + isNewChecklist = false; + } + else + { + await ChecklistService.UpdateChecklistAsync(checklist); + } - await ChecklistService.UpdateChecklistAsync(checklist); await LoadChecklist(); // Reload to get navigation properties - + successMessage = "Property and lease assigned successfully."; } catch (Exception ex) @@ -622,7 +643,8 @@ else } catch (Exception ex) { - errorMessage = $"Error saving progress: {ex.Message}"; + errorMessage = $"Error saving progress: {$"{ex.Message} - {ex.InnerException?.Message}"}"; + ToastService.ShowError(errorMessage); } finally { @@ -677,7 +699,8 @@ else } catch (Exception ex) { - errorMessage = $"Error completing checklist: {ex.Message}"; + errorMessage = $"Error completing checklist: {$"{ex.Message} - {ex.InnerException?.Message}"}"; + ToastService.ShowError(errorMessage); isSaving = false; } } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor index 2bd9a40..4dd0971 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor @@ -69,8 +69,8 @@