From 782b7b2026d9840092f1767e7fec7430d3268126 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 6 Dec 2025 10:55:04 -0600 Subject: [PATCH 01/13] Add feature branch workflow to copilot instructions --- .../.github/copilot-instructions.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) 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. From e55bddf6a9111a643beca87b219cdfde7d8bcfb5 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 6 Dec 2025 11:03:06 -0600 Subject: [PATCH 02/13] Complete ApplicationWorkflowService with all 12 workflow methods - Added CompleteScreeningAsync for updating screening results - Added GenerateLeaseOfferAsync with competing app denial - Added AcceptLeaseOfferAsync with tenant/lease creation - Added DeclineLeaseOfferAsync with property rollback - Added ExpireLeaseOfferAsync for scheduled expiration - Added ScreeningResultModel and LeaseOfferModel - All methods wrapped in transactions with audit logging - Smart property status rollback when no active apps --- .../Workflows/ApplicationWorkflowService.cs | 483 ++++++++++++++++++ 1 file changed, 483 insertions(+) diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs index a763330..815de6f 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs @@ -503,6 +503,460 @@ await LogTransitionAsync( }); } + /// + /// Updates screening results after background/credit checks are completed. + /// Does not automatically approve - requires manual ApproveApplicationAsync call. + /// + public async Task CompleteScreeningAsync( + int 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( + int 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 + { + OrganizationId = orgId.ToString(), + 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); + await _context.SaveChangesAsync(); // Save to get ID + + // 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.ToString() && + (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. + /// + public async Task> AcceptLeaseOfferAsync(int 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.ToString() && + !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 + { + OrganizationId = orgId.ToString(), + 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); + await _context.SaveChangesAsync(); // Save to get tenant ID + + // Create lease + var lease = new Lease + { + OrganizationId = orgId.ToString(), + PropertyId = leaseOffer.PropertyId, + TenantId = tenant.Id, + StartDate = leaseOffer.StartDate, + EndDate = leaseOffer.EndDate, + MonthlyRent = leaseOffer.MonthlyRent, + SecurityDeposit = leaseOffer.SecurityDeposit, + Terms = leaseOffer.Terms, + Status = "Active", + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.Leases.Add(lease); + await _context.SaveChangesAsync(); // Save to get lease ID + + // 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"); + + 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(int 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.ToString() && + !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(int 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.ToString() && + !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 @@ -639,4 +1093,33 @@ 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; } + } } From aaa15c4b19d1a2b14dab3be5bb20d068a0cf93a2 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 6 Dec 2025 11:26:52 -0600 Subject: [PATCH 03/13] Integrate ApplicationWorkflowService with UI components - Updated SubmitApplication.razor to use WorkflowService.SubmitApplicationAsync - Updated ReviewApplication.razor to use workflow methods for approve/deny/withdraw/screening - Updated GenerateLeaseOffer.razor to use WorkflowService.GenerateLeaseOfferAsync - Updated ViewLeaseOffer.razor to use AcceptLeaseOfferAsync/DeclineLeaseOfferAsync - All UI components now use workflow service for transactional safety and audit logging - Removed manual status management code from UI layer - Simplified UI error handling with WorkflowResult pattern --- .../Pages/GenerateLeaseOffer.razor | 74 ++------- .../Pages/ReviewApplication.razor | 48 +++--- .../Pages/SubmitApplication.razor | 31 ++-- .../LeaseOffers/Pages/ViewLeaseOffer.razor | 150 +++--------------- 4 files changed, 78 insertions(+), 225 deletions(-) diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor index 4facace..b018a76 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor @@ -4,11 +4,13 @@ @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 @@ -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/ReviewApplication.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor index 8eb4bcc..7983145 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor @@ -4,11 +4,13 @@ @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 @@ -830,23 +832,19 @@ try { - var newScreening = new ApplicationScreening - { - RentalApplicationId = ApplicationId, - BackgroundCheckRequested = true, - BackgroundCheckRequestedOn = DateTime.UtcNow, - CreditCheckRequested = true, - CreditCheckRequestedOn = DateTime.UtcNow, - OverallResult = ApplicationConstants.ScreeningResults.Pending, - OrganizationId = organizationId, - CreatedOn = DateTime.UtcNow, - CreatedBy = userId - }; - - screening = await PropertyService.CreateScreeningAsync(newScreening); - successMessage = "Screening initiated successfully! Background and credit checks have been requested."; + // Request both background and credit checks by default + var result = await WorkflowService.InitiateScreeningAsync(ApplicationId, true, true); - await LoadApplication(); + if (result.Success) + { + screening = result.Data; + successMessage = "Screening initiated successfully! Background and credit checks have been requested."; + await LoadApplication(); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } } catch (Exception ex) { @@ -867,16 +865,16 @@ try { - var result = await PropertyService.ApproveApplicationAsync(ApplicationId); + var result = await WorkflowService.ApproveApplicationAsync(ApplicationId); - if (result) + if (result.Success) { ToastService.ShowSuccess("Application approved! You can now generate a lease offer."); Navigation.NavigateTo($"/propertymanagement/applications/{ApplicationId}/generate-lease-offer"); } else { - errorMessage = "Failed to approve application."; + errorMessage = string.Join(", ", result.Errors); } } catch (Exception ex) @@ -898,9 +896,9 @@ try { - var result = await PropertyService.DenyApplicationAsync(ApplicationId, denyReason); + var result = await WorkflowService.DenyApplicationAsync(ApplicationId, denyReason); - if (result) + if (result.Success) { ToastService.ShowInfo("Application denied."); showDenyModal = false; @@ -908,7 +906,7 @@ } else { - errorMessage = "Failed to deny application."; + errorMessage = string.Join(", ", result.Errors); } } catch (Exception ex) @@ -930,9 +928,9 @@ try { - var result = await PropertyService.WithdrawApplicationAsync(ApplicationId, withdrawReason); + var result = await WorkflowService.WithdrawApplicationAsync(ApplicationId, withdrawReason ?? "Withdrawn by applicant"); - if (result) + if (result.Success) { ToastService.ShowInfo("Application withdrawn."); showWithdrawModal = false; @@ -940,7 +938,7 @@ } else { - errorMessage = "Failed to withdraw application."; + errorMessage = string.Join(", ", result.Errors); } } catch (Exception ex) diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor index 0d796c7..d69fbd7 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor @@ -4,11 +4,13 @@ @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 @@ -450,13 +452,9 @@ try { - var application = new RentalApplication + // 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,16 +480,23 @@ // 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); - 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) { diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor b/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor index 2c90f49..697d617 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor @@ -4,6 +4,7 @@ @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.Infrastructure.Data @using Aquiis.SimpleStart.Core.Constants @using Microsoft.AspNetCore.Authorization @@ -11,6 +12,7 @@ @using Microsoft.EntityFrameworkCore @inject PropertyManagementService PropertyService +@inject ApplicationWorkflowService WorkflowService @inject ApplicationDbContext DbContext @inject NavigationManager Navigation @inject AuthenticationStateProvider AuthStateProvider @@ -420,137 +422,30 @@ private async Task AcceptOffer() { - if (leaseOffer == null || leaseOffer.ProspectiveTenant == null || leaseOffer.Property == null) return; + if (leaseOffer == null) return; isSubmitting = true; errorMessage = string.Empty; try { - // 1. Create Tenant from ProspectiveTenant - var tenant = new Tenant - { - FirstName = leaseOffer.ProspectiveTenant.FirstName, - LastName = leaseOffer.ProspectiveTenant.LastName, - Email = leaseOffer.ProspectiveTenant.Email, - PhoneNumber = leaseOffer.ProspectiveTenant.Phone, - DateOfBirth = leaseOffer.ProspectiveTenant.DateOfBirth, - IdentificationNumber = leaseOffer.ProspectiveTenant.IdentificationNumber ?? string.Empty, - ProspectiveTenantId = leaseOffer.ProspectiveTenantId, - EmergencyContactName = string.Empty, - EmergencyContactPhone = null, - - }; - - var createdTenant = await PropertyService.AddTenantAsync(tenant); - - // 2. Create Lease with reference to LeaseOffer and Tenant - var lease = new Lease - { - PropertyId = leaseOffer.PropertyId, - OrganizationId = createdTenant.OrganizationId, - TenantId = createdTenant.Id, - LeaseOfferId = leaseOffer.Id, - StartDate = leaseOffer.StartDate, - EndDate = leaseOffer.EndDate, - MonthlyRent = leaseOffer.MonthlyRent, - SecurityDeposit = leaseOffer.SecurityDeposit, - Terms = leaseOffer.Terms ?? string.Empty, - Status = ApplicationConstants.LeaseStatuses.Active, - SignedOn = DateTime.UtcNow, - - }; - - var createdLease = await PropertyService.AddLeaseAsync(lease); - - // 3. Update LeaseOffer status to Accepted - leaseOffer.Status = "Accepted"; - leaseOffer.RespondedOn = DateTime.UtcNow; - leaseOffer.ResponseNotes = "Lease offer accepted and converted to active lease"; - leaseOffer.ConvertedLeaseId = createdLease?.Id; + var result = await WorkflowService.AcceptLeaseOfferAsync(leaseOffer.Id); - - await PropertyService.UpdateLeaseOfferAsync(leaseOffer); - - // 4. Update RentalApplication status - var application = await PropertyService.GetRentalApplicationByIdAsync(leaseOffer.RentalApplicationId); - if (application != null) + if (result.Success) { - application.Status = ApplicationConstants.ApplicationStatuses.LeaseAccepted; - - await PropertyService.UpdateRentalApplicationAsync(application); - } - - // 5. Update Property status to Occupied - var property = await PropertyService.GetPropertyByIdAsync(leaseOffer.PropertyId); - if (property != null) - { - property.Status = ApplicationConstants.PropertyStatuses.Occupied; - - await PropertyService.UpdatePropertyAsync(property); - } - - // 6. Update ProspectiveTenant status - var prospective = leaseOffer.ProspectiveTenant; - if (prospective != null) - { - prospective.Status = ApplicationConstants.ProspectiveStatuses.ConvertedToTenant; + ToastService.ShowSuccess("Lease offer accepted! Tenant and lease created successfully."); + showAcceptModal = false; - await PropertyService.UpdateProspectiveTenantAsync(prospective); - } - - // 7. Add Security Deposit to Investment Pool - // CRITICAL: Collect security deposit - this MUST succeed before proceeding - SecurityDeposit securityDeposit = new(); - try - { - securityDeposit = await SecurityDepositService.CollectSecurityDepositAsync( - lease.Id, - lease.SecurityDeposit, - "Online Payment", - "Security deposit collected upon lease acceptance", - tenant.Id); // Pass the newly created tenant ID - } - catch (Exception depositEx) - { - var message = depositEx.Message + (depositEx.InnerException != null ? $" - {depositEx.InnerException}" : string.Empty); - errorMessage = $"CRITICAL ERROR: Failed to collect security deposit. Lease acceptance aborted. Error: {message}"; - isSubmitting = false; - ToastService.ShowError(errorMessage); - Console.WriteLine(errorMessage); - //return; - } - - if(securityDeposit == null || securityDeposit.Id == 0) - { - errorMessage = "CRITICAL ERROR: Security deposit record not created. Lease acceptance aborted."; - isSubmitting = false; - ToastService.ShowError(errorMessage); - Console.WriteLine(errorMessage); - } else { - // Add deposit to investment pool (will start earning dividends) - try - { - await SecurityDepositService.AddToInvestmentPoolAsync(securityDeposit.Id); - } - catch (Exception poolEx) + // Navigate to the newly created lease + if (result.Data != null) { - // Non-critical: deposit collected but not added to pool yet - // Can be added manually later from Security Deposits page - var message = poolEx.Message + (poolEx.InnerException != null ? $" - {poolEx.InnerException}" : string.Empty); - ToastService.ShowWarning($"Security deposit collected but not added to investment pool: {message}"); - //return; - Console.WriteLine($"Warning: Security deposit collected but not added to investment pool: {message}"); + Navigation.NavigateTo($"/propertymanagement/leases/view/{result.Data.Id}"); } } - - ToastService.ShowSuccess("Lease offer accepted! Tenant and lease created successfully."); - showAcceptModal = false; - - // Navigate to the newly created lease - if (createdLease != null) + else { - Navigation.NavigateTo($"/propertymanagement/leases/view/{createdLease.Id}"); + errorMessage = string.Join(", ", result.Errors); + ToastService.ShowError($"Failed to accept lease offer: {errorMessage}"); } } catch (Exception ex) @@ -577,15 +472,18 @@ try { - leaseOffer.Status = "Declined"; - leaseOffer.RespondedOn = DateTime.UtcNow; - leaseOffer.ResponseNotes = declineReason; + var result = await WorkflowService.DeclineLeaseOfferAsync(leaseOffer.Id, declineReason ?? "Declined by applicant"); - await PropertyService.UpdateLeaseOfferAsync(leaseOffer); - - ToastService.ShowSuccess("Lease offer declined."); - showDeclineModal = false; - await LoadLeaseOffer(); + if (result.Success) + { + ToastService.ShowSuccess("Lease offer declined."); + showDeclineModal = false; + await LoadLeaseOffer(); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } } catch (Exception ex) { From 3e929404206ef7ecdcc12d40b514d959c76a0327 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 6 Dec 2025 11:40:01 -0600 Subject: [PATCH 04/13] Fix OrganizationId type mismatch in workflow services - Changed GetActiveOrganizationIdAsync to return string instead of int - Updated ValidateOrganizationOwnershipAsync to use string OrganizationId - Replaced all orgId.ToString() calls with orgId throughout ApplicationWorkflowService - Fixes error: 'The input string 'GUID' was not in a correct format' - OrganizationId is a GUID stored as string, not an integer --- .../Workflows/ApplicationWorkflowService.cs | 36 +++++++++---------- .../Services/Workflows/BaseWorkflowService.cs | 7 ++-- .../Checklists/Pages/Complete.razor | 1 - .../Checklists/Pages/View.razor | 6 +++- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs index 815de6f..6bb938d 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs @@ -115,14 +115,14 @@ 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(), + OrganizationId = orgId, ProspectiveTenantId = prospectId, PropertyId = propertyId, Status = ApplicationConstants.ApplicationStatuses.Submitted, @@ -158,7 +158,7 @@ public async Task> SubmitApplicationAsync( // 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 +169,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) { @@ -282,7 +282,7 @@ public async Task> InitiateScreeningAsync( // Create screening record var screening = new ApplicationScreening { - OrganizationId = orgId.ToString(), + OrganizationId = orgId, RentalApplicationId = applicationId, BackgroundCheckRequested = requestBackgroundCheck, BackgroundCheckRequestedOn = requestBackgroundCheck ? DateTime.UtcNow : null, @@ -605,7 +605,7 @@ public async Task> GenerateLeaseOfferAsync( // Create lease offer var leaseOffer = new LeaseOffer { - OrganizationId = orgId.ToString(), + OrganizationId = orgId, RentalApplicationId = applicationId, PropertyId = property.Id, ProspectiveTenantId = application.ProspectiveTenantId, @@ -648,7 +648,7 @@ public async Task> GenerateLeaseOfferAsync( var competingApps = await _context.RentalApplications .Where(a => a.PropertyId == property.Id && a.Id != applicationId && - a.OrganizationId == orgId.ToString() && + a.OrganizationId == orgId && (a.Status == ApplicationConstants.ApplicationStatuses.Submitted || a.Status == ApplicationConstants.ApplicationStatuses.UnderReview || a.Status == ApplicationConstants.ApplicationStatuses.Screening || @@ -719,7 +719,7 @@ public async Task> AcceptLeaseOfferAsync(int leaseOfferId) .ThenInclude(a => a.ProspectiveTenant) .Include(lo => lo.Property) .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && - lo.OrganizationId == orgId.ToString() && + lo.OrganizationId == orgId && !lo.IsDeleted); if (leaseOffer == null) @@ -738,7 +738,7 @@ public async Task> AcceptLeaseOfferAsync(int leaseOfferId) // Convert prospect to tenant var tenant = new Tenant { - OrganizationId = orgId.ToString(), + OrganizationId = orgId, FirstName = prospect.FirstName, LastName = prospect.LastName, Email = prospect.Email, @@ -757,7 +757,7 @@ public async Task> AcceptLeaseOfferAsync(int leaseOfferId) // Create lease var lease = new Lease { - OrganizationId = orgId.ToString(), + OrganizationId = orgId, PropertyId = leaseOffer.PropertyId, TenantId = tenant.Id, StartDate = leaseOffer.StartDate, @@ -841,7 +841,7 @@ public async Task DeclineLeaseOfferAsync(int leaseOfferId, strin .ThenInclude(a => a.ProspectiveTenant) .Include(lo => lo.Property) .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && - lo.OrganizationId == orgId.ToString() && + lo.OrganizationId == orgId && !lo.IsDeleted); if (leaseOffer == null) @@ -906,7 +906,7 @@ public async Task ExpireLeaseOfferAsync(int leaseOfferId) .ThenInclude(a => a.ProspectiveTenant) .Include(lo => lo.Property) .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && - lo.OrganizationId == orgId.ToString() && + lo.OrganizationId == orgId && !lo.IsDeleted); if (leaseOffer == null) @@ -970,7 +970,7 @@ await LogTransitionAsync( .Include(a => a.Screening) .FirstOrDefaultAsync(a => a.Id == applicationId && - a.OrganizationId == orgId.ToString() && + a.OrganizationId == orgId && !a.IsDeleted); } @@ -983,7 +983,7 @@ private async Task ValidateApplicationSubmissionAsync( // 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"); @@ -992,7 +992,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"); @@ -1005,7 +1005,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 && @@ -1042,7 +1042,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); @@ -1050,7 +1050,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) { diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs index 2890833..4b3d6c8 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs @@ -149,7 +149,7 @@ protected async Task ValidateOrganizationOwnershipAsync( // 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, "OrganizationId") == activeOrgId) .Where(e => EF.Property(e, "IsDeleted") == false) .FirstOrDefaultAsync(); @@ -167,10 +167,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() ?? string.Empty; } } } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor index b4bd97c..b30ed8e 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor @@ -223,7 +223,6 @@ else } - }
diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor index 873de34..618e83e 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor @@ -290,7 +290,11 @@ else
-
\n
\n
\n
Summary
\n
+
+
+
+
Summary
+
@if (checklist.Items != null && checklist.Items.Any()) { From 7a475c954838b9b4b5525143fc0326b786aa39b8 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 6 Dec 2025 12:30:46 -0600 Subject: [PATCH 05/13] Fix WorkflowAuditLog OrganizationId type to string - Changed WorkflowAuditLog.OrganizationId from int to string - Updated LogTransitionAsync to use string without parsing - Updated GetAuditHistoryAsync to use string comparison - Created and applied migration ChangeWorkflowAuditLogOrganizationIdToString - Fixes second GUID parsing error in workflow audit logging --- .../Services/Workflows/BaseWorkflowService.cs | 4 +- .../Services/Workflows/WorkflowAuditLog.cs | 2 +- ...AuditLogOrganizationIdToString.Designer.cs | 3964 +++++++++++++++++ ...eWorkflowAuditLogOrganizationIdToString.cs | 34 + .../ApplicationDbContextModelSnapshot.cs | 5 +- 5 files changed, 4004 insertions(+), 5 deletions(-) create mode 100644 Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.Designer.cs create mode 100644 Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.cs diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs index 4b3d6c8..2ccf536 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs @@ -110,7 +110,7 @@ protected async Task LogTransitionAsync( Reason = reason, PerformedBy = userId, PerformedOn = DateTime.UtcNow, - OrganizationId = int.Parse(activeOrgId ?? "0"), + OrganizationId = activeOrgId ?? string.Empty, Metadata = metadata != null ? JsonSerializer.Serialize(metadata) : null, CreatedOn = DateTime.UtcNow, CreatedBy = userId @@ -131,7 +131,7 @@ public async Task> GetAuditHistoryAsync( 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(); } diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs b/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs index f115ba5..8cd6df4 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs @@ -51,7 +51,7 @@ public class WorkflowAuditLog : BaseModel /// /// Organization context for the workflow action /// - public required int OrganizationId { get; set; } + public required string OrganizationId { get; set; } /// /// Additional context data (JSON serialized) diff --git a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.Designer.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.Designer.cs new file mode 100644 index 0000000..d34565a --- /dev/null +++ b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.Designer.cs @@ -0,0 +1,3964 @@ +// +using System; +using Aquiis.SimpleStart.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.SimpleStart.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251206182946_ChangeWorkflowAuditLogOrganizationIdToString")] + partial class ChangeWorkflowAuditLogOrganizationIdToString + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.SimpleStart.Application.Services.Workflows.WorkflowAuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ApplicationScreening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreditCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreditScore") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallResult") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("INTEGER"); + + b.Property("ResultNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OverallResult"); + + b.HasIndex("RentalApplicationId") + .IsUnique(); + + b.ToTable("ApplicationScreenings"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.CalendarEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("EndOn") + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("INTEGER"); + + b.Property("SourceEntityId") + .HasColumnType("INTEGER"); + + b.Property("SourceEntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StartOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChecklistTemplateId") + .HasColumnType("INTEGER"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("INTEGER"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("INTEGER"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = 1, + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Property Tour", + OrganizationId = "" + }, + new + { + Id = 2, + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Move-In", + OrganizationId = "" + }, + new + { + Id = 3, + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Move-Out", + OrganizationId = "" + }, + new + { + Id = 4, + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Open House", + OrganizationId = "" + }); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = 1, + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 2, + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 3, + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 4, + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = 5, + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = 6, + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = 7, + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = 8, + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = 9, + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = 10, + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = 11, + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 11, + ItemText = "Showed water heater location", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = 12, + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = 13, + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = 14, + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = 15, + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = true, + SectionOrder = 6 + }, + new + { + Id = 16, + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = true, + SectionOrder = 6 + }, + new + { + Id = 17, + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = 18, + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 18, + ItemText = "Explained pet policy", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = 19, + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = 20, + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = 21, + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = 22, + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = 23, + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = 1, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = 24, + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = 2, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Document property condition", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 25, + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = 2, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 26, + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = 2, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 27, + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = 3, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Inspect property condition", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 28, + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = 3, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 29, + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = 3, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 30, + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = 4, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 31, + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = 4, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Prepare information packets", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = 32, + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = 4, + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + LastModifiedBy = "", + OrganizationId = "", + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("INTEGER"); + + b.Property("PropertyId") + .HasColumnType("INTEGER"); + + b.Property("TenantId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Inspection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("INTEGER"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("INTEGER"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("INTEGER"); + + b.Property("PropertyId") + .HasColumnType("INTEGER"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("INTEGER"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConvertedLeaseId") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("INTEGER"); + + b.Property("ProspectiveTenantId") + .HasColumnType("INTEGER"); + + b.Property("RentalApplicationId") + .HasColumnType("INTEGER"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("INTEGER"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("INTEGER"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Note", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => + { + b.Property("Id") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("INTEGER"); + + b.Property("InvoiceId") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("INTEGER"); + + b.Property("ProspectiveTenantId") + .HasColumnType("INTEGER"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("INTEGER"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("INTEGER"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("INTEGER"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tour", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CalendarEventId") + .HasColumnType("INTEGER"); + + b.Property("ChecklistId") + .HasColumnType("INTEGER"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("INTEGER"); + + b.Property("ProspectiveTenantId") + .HasColumnType("INTEGER"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.UserOrganization", b => + { + b.Property("Id") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrantedBy"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ActiveOrganizationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginIP") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("LoginCount") + .HasColumnType("INTEGER"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PreviousLoginDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.SimpleStart.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Document", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Note", b => + { + b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", "User") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => + { + b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithOne("Application") + .HasForeignKey("Aquiis.SimpleStart.Core.Entities.RentalApplication", "ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.UserOrganization", b => + { + b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("GrantedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") + .WithMany("UserOrganizations") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + + b.Navigation("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Application"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.cs new file mode 100644 index 0000000..4d240ce --- /dev/null +++ b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.SimpleStart.Infrastructure.Data.Migrations +{ + /// + public partial class ChangeWorkflowAuditLogOrganizationIdToString : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "OrganizationId", + table: "WorkflowAuditLogs", + type: "TEXT", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "OrganizationId", + table: "WorkflowAuditLogs", + type: "INTEGER", + nullable: false, + oldClrType: typeof(string), + oldType: "TEXT"); + } + } +} diff --git a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs index fbe8ab2..ae4b331 100644 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -58,8 +58,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Metadata") .HasColumnType("TEXT"); - b.Property("OrganizationId") - .HasColumnType("INTEGER"); + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("TEXT"); b.Property("PerformedBy") .IsRequired() From 4b7efa5dedd2de5dbf7a4e0eaf2c9afe33c80e8b Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 6 Dec 2025 12:33:41 -0600 Subject: [PATCH 06/13] Fix transaction atomicity in workflow operations - Removed all intermediate SaveChangesAsync calls in ApplicationWorkflowService - SaveChanges now only occurs once at transaction commit in ExecuteWorkflowAsync - Ensures all-or-nothing execution - prevents partial updates on failure - EF Core assigns IDs during tracking, available for use before final commit - Fixes issue where property/prospect status changed despite workflow failure Critical fix: Previously calling SaveChangesAsync in the middle of operations committed those changes even when later steps failed, breaking transaction rollback. --- .../Services/Workflows/ApplicationWorkflowService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs index 6bb938d..afb576d 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs @@ -154,7 +154,7 @@ 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 @@ -623,7 +623,7 @@ public async Task> GenerateLeaseOfferAsync( }; _context.LeaseOffers.Add(leaseOffer); - await _context.SaveChangesAsync(); // Save to get ID + // Note: EF Core will assign ID when transaction commits // Update application var oldAppStatus = application.Status; @@ -752,7 +752,7 @@ public async Task> AcceptLeaseOfferAsync(int leaseOfferId) }; _context.Tenants.Add(tenant); - await _context.SaveChangesAsync(); // Save to get tenant ID + // Note: EF Core will assign ID when transaction commits // Create lease var lease = new Lease @@ -771,7 +771,7 @@ public async Task> AcceptLeaseOfferAsync(int leaseOfferId) }; _context.Leases.Add(lease); - await _context.SaveChangesAsync(); // Save to get lease ID + // Note: EF Core will assign ID when transaction commits // Update lease offer leaseOffer.Status = "Accepted"; From 911edf8820a6b30c42e0adabc968a717c2accb3b Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sun, 7 Dec 2025 11:28:19 -0600 Subject: [PATCH 07/13] Fix application workflow issues - Fixed property selection validation in SubmitApplication.razor - Removed conflicting @onchange handler - Added @bind-Value:after to sync selectedProperty - Added ValidationSummary for better error visibility - Added console logging for debugging - Fixed InitiateScreeningAsync workflow transition - Auto-transitions from Submitted to UnderReview before screening - Eliminates need for manual 'Mark Under Review' step - More user-friendly workflow progression - Logs auto-transition in audit trail --- .../Workflows/ApplicationWorkflowService.cs | 24 ++++++++++--- .../Pages/SubmitApplication.razor | 34 ++++++++++++++++--- Aquiis.SimpleStart/wwwroot/js/theme.js | 4 +-- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs index afb576d..f339949 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs @@ -258,10 +258,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,9 +295,6 @@ 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 { diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor index d69fbd7..2de420c 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor @@ -79,8 +79,9 @@
} - + +
Applicant Information
@@ -104,7 +105,7 @@
Property Selection
- + @foreach (var property in availableProperties) { @@ -428,30 +429,52 @@ } } + private void UpdateSelectedProperty() + { + if (applicationModel.PropertyId > 0) + { + 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) { + applicationModel.PropertyId = propertyId; selectedProperty = availableProperties.FirstOrDefault(p => p.Id == propertyId); } else { + applicationModel.PropertyId = 0; 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 { + Console.WriteLine($"Submitting application for prospect {ProspectId}, property {applicationModel.PropertyId}"); + // Use ApplicationWorkflowService to submit application var submissionModel = new Aquiis.SimpleStart.Application.Services.Workflows.ApplicationSubmissionModel { @@ -488,6 +511,8 @@ applicationModel.PropertyId, submissionModel); + Console.WriteLine($"Workflow result: Success={result.Success}, Errors={string.Join(", ", result.Errors)}"); + if (result.Success) { ToastService.ShowSuccess("Application submitted successfully! You will be notified once reviewed."); @@ -501,6 +526,7 @@ catch (Exception ex) { errorMessage = $"Error submitting application: {ex.Message}"; + Console.WriteLine($"Exception in HandleSubmitApplication: {ex}"); } finally { diff --git a/Aquiis.SimpleStart/wwwroot/js/theme.js b/Aquiis.SimpleStart/wwwroot/js/theme.js index 677fc03..f35b2aa 100644 --- a/Aquiis.SimpleStart/wwwroot/js/theme.js +++ b/Aquiis.SimpleStart/wwwroot/js/theme.js @@ -18,7 +18,7 @@ window.themeManager = { getTheme: function () { const theme = localStorage.getItem("theme") || "light"; - console.log("Getting theme from localStorage:", theme); + //console.log("Getting theme from localStorage:", theme); return theme; }, @@ -44,7 +44,7 @@ if (typeof localStorage !== "undefined") { document.documentElement.style.display = "none"; void document.documentElement.offsetHeight; document.documentElement.style.display = ""; - console.log("Theme re-applied after DOM mutation:", currentTheme); + //console.log("Theme re-applied after DOM mutation:", currentTheme); } }); From 755c5c3f66f31d1e1ca0968a5cb1706eac62108b Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Mon, 8 Dec 2025 16:55:12 -0600 Subject: [PATCH 08/13] See REVISIONS.md for details --- .../Services/PropertyManagementService.cs | 21 + .../Workflows/ApplicationWorkflowService.cs | 32 +- .../Services/Workflows/BaseWorkflowService.cs | 36 +- .../Core/Constants/ApplicationConstants.cs | 50 ++- .../Pages/GenerateLeaseOffer.razor | 2 +- .../Pages/ReviewApplication.razor | 10 +- .../Inspections/Pages/Create.razor | 282 +++++++++--- .../LeaseOffers/Pages/ViewLeaseOffer.razor | 109 +++-- .../Leases/Pages/Leases.razor | 14 +- .../Pages/CreateMaintenanceRequest.razor | 33 +- .../Tenants/Pages/Tenants.razor | 4 +- Aquiis.SimpleStart/wwwroot/js/theme.js | 10 +- Aquiis.Tests/AccountTests.cs | 412 ++++++++++++++++++ .../{AddProperty.cs => ApplicationTests.cs} | 48 +- Aquiis.Tests/RemoveProperty.cs | 37 -- .../{UnitTest1.cs => UnitTest1.cs.bak} | 2 +- 16 files changed, 921 insertions(+), 181 deletions(-) create mode 100644 Aquiis.Tests/AccountTests.cs rename Aquiis.Tests/{AddProperty.cs => ApplicationTests.cs} (60%) delete mode 100644 Aquiis.Tests/RemoveProperty.cs rename Aquiis.Tests/{UnitTest1.cs => UnitTest1.cs.bak} (99%) diff --git a/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs b/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs index af509aa..13b6948 100644 --- a/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs +++ b/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs @@ -504,6 +504,27 @@ public async Task> GetLeasesByPropertyIdAsync(int propertyId) .ToList(); } + public async Task> GetCurrentAndUpcomingLeasesByPropertyIdAsync(int 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 == "Active" || l.Status == "Pending")) + .ToListAsync(); + } + public async Task> GetActiveLeasesByPropertyIdAsync(int propertyId) { var _userId = await _userContext.GetUserIdAsync(); diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs index f339949..02e9f6e 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs @@ -722,8 +722,14 @@ await LogTransitionAsync( /// /// 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(int leaseOfferId) + public async Task> AcceptLeaseOfferAsync( + int leaseOfferId, + string depositPaymentMethod, + DateTime depositPaymentDate, + string? depositReferenceNumber = null, + string? depositNotes = null) { return await ExecuteWorkflowAsync(async () => { @@ -775,13 +781,15 @@ public async Task> AcceptLeaseOfferAsync(int leaseOfferId) { OrganizationId = orgId, PropertyId = leaseOffer.PropertyId, - TenantId = tenant.Id, + 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 = "Active", + SignedOn = DateTime.UtcNow, CreatedBy = userId, CreatedOn = DateTime.UtcNow }; @@ -789,6 +797,26 @@ public async Task> AcceptLeaseOfferAsync(int leaseOfferId) _context.Leases.Add(lease); // Note: EF Core will assign ID when transaction commits + // Create security deposit record + var securityDeposit = new SecurityDeposit + { + 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; diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs index 2ccf536..491f66d 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}"); } } diff --git a/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs b/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs index e38814e..0dcb7b8 100644 --- a/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs +++ b/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs @@ -237,15 +237,29 @@ public static class LeaseStatuses { public const string Pending = "Pending"; 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, Active, Declined, + Renewed, + Interrupted, Terminated, Expired }; @@ -281,11 +295,11 @@ 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 Occupied = "Occupied"; - public const string UnderRenovation = "UnderRenovation"; - public const string OffMarket = "OffMarket"; + public const string UnderRenovation = "Under Renovation"; + public const string OffMarket = "Off Market"; public static IReadOnlyList AllPropertyStatuses { get; } = new List { @@ -432,15 +446,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 +513,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 +530,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 +560,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 +577,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 +608,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 +622,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"; diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor index b018a76..b7723be 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor @@ -344,7 +344,7 @@ if (result.Success) { ToastService.ShowSuccess("Lease offer generated successfully!"); - Navigation.NavigateTo($"/propertymanagement/leaseoffers/view/{result.Data.Id}"); + Navigation.NavigateTo($"/propertymanagement/leaseoffers/view/{result.Data!.Id}"); } else { diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor index 7983145..2089fca 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor @@ -401,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) {
- - - - - - - + + + + + + +
@@ -132,11 +133,11 @@ else
- - - - - + + + + +
@@ -149,10 +150,10 @@ else
- - - - + + + +
@@ -165,10 +166,10 @@ else
- - - - + + + +
@@ -181,11 +182,11 @@ else
- - - - - + + + + +
@@ -197,7 +198,7 @@ else
- + @@ -206,11 +207,11 @@ else
- +
- +
@@ -261,7 +262,7 @@ else public int? PropertyIdFromQuery { get; set; } private Property? property; - private Inspection inspection = new(); + private InspectionModel model = new(); private string? errorMessage; private string? successMessage; private bool isSaving = false; @@ -274,16 +275,16 @@ else PropertyId = PropertyIdFromQuery; } - // Get the current user's organization and user ID first + @* // Get the current user's organization and user ID first var organizationId = await UserContext.GetActiveOrganizationIdAsync(); var userId = await UserContext.GetUserIdAsync(); - var userEmail = await UserContext.GetUserEmailAsync(); + var userEmail = await UserContext.GetUserEmailAsync(); *@ - if (organizationId == null || string.IsNullOrEmpty(userId)) + @* if (organizationId == null || string.IsNullOrEmpty(userId)) { errorMessage = "Unable to determine user context. Please log in again."; return; - } + } *@ if (PropertyId.HasValue) { @@ -295,13 +296,13 @@ else return; } - inspection.PropertyId = PropertyId.Value; + model.PropertyId = PropertyId.Value; // Check if there's an active lease var activeLeases = await PropertyManagementService.GetActiveLeasesByPropertyIdAsync(PropertyId.Value); if (activeLeases.Any()) { - inspection.LeaseId = activeLeases.First().Id; + model.LeaseId = activeLeases.First().Id; } } else @@ -318,6 +319,72 @@ else errorMessage = null; successMessage = null; + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + // Create inspection entity from model + var inspection = new Inspection + { + PropertyId = model.PropertyId, + LeaseId = model.LeaseId, + CompletedOn = model.CompletedOn, + InspectionType = model.InspectionType, + InspectedBy = model.InspectedBy, + ExteriorRoofGood = model.ExteriorRoofGood, + ExteriorRoofNotes = model.ExteriorRoofNotes, + ExteriorGuttersGood = model.ExteriorGuttersGood, + ExteriorGuttersNotes = model.ExteriorGuttersNotes, + ExteriorSidingGood = model.ExteriorSidingGood, + ExteriorSidingNotes = model.ExteriorSidingNotes, + ExteriorWindowsGood = model.ExteriorWindowsGood, + ExteriorWindowsNotes = model.ExteriorWindowsNotes, + ExteriorDoorsGood = model.ExteriorDoorsGood, + ExteriorDoorsNotes = model.ExteriorDoorsNotes, + ExteriorFoundationGood = model.ExteriorFoundationGood, + ExteriorFoundationNotes = model.ExteriorFoundationNotes, + LandscapingGood = model.LandscapingGood, + LandscapingNotes = model.LandscapingNotes, + InteriorWallsGood = model.InteriorWallsGood, + InteriorWallsNotes = model.InteriorWallsNotes, + InteriorCeilingsGood = model.InteriorCeilingsGood, + InteriorCeilingsNotes = model.InteriorCeilingsNotes, + InteriorFloorsGood = model.InteriorFloorsGood, + InteriorFloorsNotes = model.InteriorFloorsNotes, + InteriorDoorsGood = model.InteriorDoorsGood, + InteriorDoorsNotes = model.InteriorDoorsNotes, + InteriorWindowsGood = model.InteriorWindowsGood, + InteriorWindowsNotes = model.InteriorWindowsNotes, + KitchenAppliancesGood = model.KitchenAppliancesGood, + KitchenAppliancesNotes = model.KitchenAppliancesNotes, + KitchenCabinetsGood = model.KitchenCabinetsGood, + KitchenCabinetsNotes = model.KitchenCabinetsNotes, + KitchenCountersGood = model.KitchenCountersGood, + KitchenCountersNotes = model.KitchenCountersNotes, + KitchenSinkPlumbingGood = model.KitchenSinkPlumbingGood, + KitchenSinkPlumbingNotes = model.KitchenSinkPlumbingNotes, + BathroomToiletGood = model.BathroomToiletGood, + BathroomToiletNotes = model.BathroomToiletNotes, + BathroomSinkGood = model.BathroomSinkGood, + BathroomSinkNotes = model.BathroomSinkNotes, + BathroomTubShowerGood = model.BathroomTubShowerGood, + BathroomTubShowerNotes = model.BathroomTubShowerNotes, + BathroomVentilationGood = model.BathroomVentilationGood, + BathroomVentilationNotes = model.BathroomVentilationNotes, + HvacSystemGood = model.HvacSystemGood, + HvacSystemNotes = model.HvacSystemNotes, + ElectricalSystemGood = model.ElectricalSystemGood, + ElectricalSystemNotes = model.ElectricalSystemNotes, + PlumbingSystemGood = model.PlumbingSystemGood, + PlumbingSystemNotes = model.PlumbingSystemNotes, + SmokeDetectorsGood = model.SmokeDetectorsGood, + SmokeDetectorsNotes = model.SmokeDetectorsNotes, + CarbonMonoxideDetectorsGood = model.CarbonMonoxideDetectorsGood, + CarbonMonoxideDetectorsNotes = model.CarbonMonoxideDetectorsNotes, + OverallCondition = model.OverallCondition, + GeneralNotes = model.GeneralNotes, + ActionItemsRequired = model.ActionItemsRequired, + }; + // Add the inspection await PropertyManagementService.AddInspectionAsync(inspection); @@ -351,46 +418,131 @@ else private void MarkAllExteriorGood() { - inspection.ExteriorRoofGood = true; - inspection.ExteriorGuttersGood = true; - inspection.ExteriorSidingGood = true; - inspection.ExteriorWindowsGood = true; - inspection.ExteriorDoorsGood = true; - inspection.ExteriorFoundationGood = true; - inspection.LandscapingGood = true; + model.ExteriorRoofGood = true; + model.ExteriorGuttersGood = true; + model.ExteriorSidingGood = true; + model.ExteriorWindowsGood = true; + model.ExteriorDoorsGood = true; + model.ExteriorFoundationGood = true; + model.LandscapingGood = true; } private void MarkAllInteriorGood() { - inspection.InteriorWallsGood = true; - inspection.InteriorCeilingsGood = true; - inspection.InteriorFloorsGood = true; - inspection.InteriorDoorsGood = true; - inspection.InteriorWindowsGood = true; + model.InteriorWallsGood = true; + model.InteriorCeilingsGood = true; + model.InteriorFloorsGood = true; + model.InteriorDoorsGood = true; + model.InteriorWindowsGood = true; } private void MarkAllKitchenGood() { - inspection.KitchenAppliancesGood = true; - inspection.KitchenCabinetsGood = true; - inspection.KitchenCountersGood = true; - inspection.KitchenSinkPlumbingGood = true; + model.KitchenAppliancesGood = true; + model.KitchenCabinetsGood = true; + model.KitchenCountersGood = true; + model.KitchenSinkPlumbingGood = true; } private void MarkAllBathroomGood() { - inspection.BathroomToiletGood = true; - inspection.BathroomSinkGood = true; - inspection.BathroomTubShowerGood = true; - inspection.BathroomVentilationGood = true; + model.BathroomToiletGood = true; + model.BathroomSinkGood = true; + model.BathroomTubShowerGood = true; + model.BathroomVentilationGood = true; } private void MarkAllSystemsGood() { - inspection.HvacSystemGood = true; - inspection.ElectricalSystemGood = true; - inspection.PlumbingSystemGood = true; - inspection.SmokeDetectorsGood = true; - inspection.CarbonMonoxideDetectorsGood = true; + model.HvacSystemGood = true; + model.ElectricalSystemGood = true; + model.PlumbingSystemGood = true; + model.SmokeDetectorsGood = true; + model.CarbonMonoxideDetectorsGood = true; + } + + public class InspectionModel + { + [Required] + public int PropertyId { get; set; } + + public int? LeaseId { get; set; } + + [Required] + public DateTime CompletedOn { get; set; } = DateTime.Today; + + [Required] + [StringLength(50)] + public string InspectionType { get; set; } = "Routine"; + + [StringLength(100)] + public string? InspectedBy { get; set; } + + // Exterior + public bool ExteriorRoofGood { get; set; } + public string? ExteriorRoofNotes { get; set; } + public bool ExteriorGuttersGood { get; set; } + public string? ExteriorGuttersNotes { get; set; } + public bool ExteriorSidingGood { get; set; } + public string? ExteriorSidingNotes { get; set; } + public bool ExteriorWindowsGood { get; set; } + public string? ExteriorWindowsNotes { get; set; } + public bool ExteriorDoorsGood { get; set; } + public string? ExteriorDoorsNotes { get; set; } + public bool ExteriorFoundationGood { get; set; } + public string? ExteriorFoundationNotes { get; set; } + public bool LandscapingGood { get; set; } + public string? LandscapingNotes { get; set; } + + // Interior + public bool InteriorWallsGood { get; set; } + public string? InteriorWallsNotes { get; set; } + public bool InteriorCeilingsGood { get; set; } + public string? InteriorCeilingsNotes { get; set; } + public bool InteriorFloorsGood { get; set; } + public string? InteriorFloorsNotes { get; set; } + public bool InteriorDoorsGood { get; set; } + public string? InteriorDoorsNotes { get; set; } + public bool InteriorWindowsGood { get; set; } + public string? InteriorWindowsNotes { get; set; } + + // Kitchen + public bool KitchenAppliancesGood { get; set; } + public string? KitchenAppliancesNotes { get; set; } + public bool KitchenCabinetsGood { get; set; } + public string? KitchenCabinetsNotes { get; set; } + public bool KitchenCountersGood { get; set; } + public string? KitchenCountersNotes { get; set; } + public bool KitchenSinkPlumbingGood { get; set; } + public string? KitchenSinkPlumbingNotes { get; set; } + + // Bathroom + public bool BathroomToiletGood { get; set; } + public string? BathroomToiletNotes { get; set; } + public bool BathroomSinkGood { get; set; } + public string? BathroomSinkNotes { get; set; } + public bool BathroomTubShowerGood { get; set; } + public string? BathroomTubShowerNotes { get; set; } + public bool BathroomVentilationGood { get; set; } + public string? BathroomVentilationNotes { get; set; } + + // Systems + public bool HvacSystemGood { get; set; } + public string? HvacSystemNotes { get; set; } + public bool ElectricalSystemGood { get; set; } + public string? ElectricalSystemNotes { get; set; } + public bool PlumbingSystemGood { get; set; } + public string? PlumbingSystemNotes { get; set; } + public bool SmokeDetectorsGood { get; set; } + public string? SmokeDetectorsNotes { get; set; } + public bool CarbonMonoxideDetectorsGood { get; set; } + public string? CarbonMonoxideDetectorsNotes { get; set; } + + // Overall + [Required] + [StringLength(50)] + public string OverallCondition { get; set; } = "Good"; + public string? GeneralNotes { get; set; } + public string? ActionItemsRequired { get; set; } } } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor b/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor index 697d617..003f7fa 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor @@ -10,6 +10,7 @@ @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.EntityFrameworkCore +@using System.ComponentModel.DataAnnotations @inject PropertyManagementService PropertyService @inject ApplicationWorkflowService WorkflowService @@ -254,33 +255,70 @@ @@ -372,6 +410,7 @@ private string declineReason = string.Empty; private string userId = string.Empty; private string organizationId = string.Empty; + private DepositPaymentModel depositPaymentModel = new(); protected override async Task OnInitializedAsync() { @@ -429,12 +468,18 @@ try { - var result = await WorkflowService.AcceptLeaseOfferAsync(leaseOffer.Id); + var result = await WorkflowService.AcceptLeaseOfferAsync( + leaseOffer.Id, + depositPaymentModel.PaymentMethod, + depositPaymentModel.PaymentDate, + depositPaymentModel.ReferenceNumber, + depositPaymentModel.Notes); if (result.Success) { - ToastService.ShowSuccess("Lease offer accepted! Tenant and lease created successfully."); + ToastService.ShowSuccess($"Lease offer accepted! Security deposit of {leaseOffer.SecurityDeposit:C} collected via {depositPaymentModel.PaymentMethod}."); showAcceptModal = false; + depositPaymentModel = new(); // Navigate to the newly created lease if (result.Data != null) @@ -514,4 +559,16 @@ Navigation.NavigateTo("/propertymanagement/applications"); } } + + public class DepositPaymentModel + { + [Required(ErrorMessage = "Payment method is required")] + public string PaymentMethod { get; set; } = string.Empty; + + public string? ReferenceNumber { get; set; } + + public DateTime PaymentDate { get; set; } = DateTime.Today; + + public string? Notes { get; set; } + } } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Leases.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Leases.razor index 7fe42d4..cce9808 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Leases.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Leases.razor @@ -89,7 +89,7 @@ else if (!leases.Any()) else {

No Leases Found

-

Get started by creating your first lease agreement.

+

Get started by converting a lease offer to your first lease agreement.

} @@ -765,31 +765,33 @@ else private void CreateLease() { - NavigationManager.NavigateTo("/propertymanagement/leases/create"); + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); } private void CreateLeaseForTenant() { - if (TenantId.HasValue) + @* if (TenantId.HasValue) { NavigationManager.NavigateTo($"/propertymanagement/leases/create?tenantId={TenantId.Value}"); } else { NavigationManager.NavigateTo("/propertymanagement/leases/create"); - } + } *@ + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); } private void CreateLeaseForProperty() { - if (PropertyId.HasValue) + @* if (PropertyId.HasValue) { NavigationManager.NavigateTo($"/propertymanagement/leases/create?propertyId={PropertyId.Value}"); } else { NavigationManager.NavigateTo("/propertymanagement/leases/create"); - } + } *@ + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); } private void ClearFilter() diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor b/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor index 621e500..d351f18 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor @@ -51,14 +51,17 @@
- - - - @foreach (var lease in availableLeases) + +
+ @if (currentLease != null) { - + @currentLease.Tenant?.FullName - @currentLease.Status } - + else + { + No active leases + } +
@@ -210,7 +213,7 @@ private MaintenanceRequestModel maintenanceRequest = new(); private List properties = new(); - private List availableLeases = new(); + private Lease? currentLease = null; private bool isLoading = true; private bool isSaving = false; @@ -226,7 +229,7 @@ maintenanceRequest.PropertyId = PropertyId.Value; if (properties.Any()) { - await LoadLeasesForProperty(PropertyId.Value); + await LoadLeaseForProperty(PropertyId.Value); } } if (LeaseId.HasValue && LeaseId.Value > 0 && maintenanceRequest.LeaseId != LeaseId.Value) @@ -245,7 +248,7 @@ if (PropertyId.HasValue && PropertyId.Value > 0) { maintenanceRequest.PropertyId = PropertyId.Value; - await LoadLeasesForProperty(PropertyId.Value); + await LoadLeaseForProperty(PropertyId.Value); } if (LeaseId.HasValue && LeaseId.Value > 0) { @@ -262,18 +265,20 @@ { if (maintenanceRequest.PropertyId > 0) { - await LoadLeasesForProperty(maintenanceRequest.PropertyId); + await LoadLeaseForProperty(maintenanceRequest.PropertyId); } else { - availableLeases.Clear(); + currentLease = null; + maintenanceRequest.LeaseId = null; } } - private async Task LoadLeasesForProperty(int propertyId) + private async Task LoadLeaseForProperty(int propertyId) { - var allLeases = await PropertyManagementService.GetLeasesByPropertyIdAsync(propertyId); - availableLeases = allLeases.Where(l => l.Status == "Active" || l.Status == "Pending").ToList(); + var leases = await PropertyManagementService.GetCurrentAndUpcomingLeasesByPropertyIdAsync(propertyId); + currentLease = leases.FirstOrDefault(); + maintenanceRequest.LeaseId = currentLease?.Id; } private async Task HandleValidSubmit() diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Tenants.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Tenants.razor index d73262f..f9fb872 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Tenants.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Tenants.razor @@ -31,7 +31,7 @@ else if (!tenants.Any()) {

No Tenants Found

-

Get started by adding your first tenant to the system.

+

Get started by converting a Prospective Tenant to your first tenant in the system.

} @@ -345,7 +345,7 @@ else private void CreateTenant() { - Navigation.NavigateTo("/propertymanagement/tenants/create"); + Navigation.NavigateTo("/propertymanagement/prospectivetenants"); } private void ViewTenant(int id) diff --git a/Aquiis.SimpleStart/wwwroot/js/theme.js b/Aquiis.SimpleStart/wwwroot/js/theme.js index f35b2aa..168c1bf 100644 --- a/Aquiis.SimpleStart/wwwroot/js/theme.js +++ b/Aquiis.SimpleStart/wwwroot/js/theme.js @@ -1,6 +1,6 @@ window.themeManager = { setTheme: function (theme) { - console.log("Setting theme to:", theme); + //console.log("Setting theme to:", theme); document.documentElement.setAttribute("data-bs-theme", theme); localStorage.setItem("theme", theme); @@ -10,10 +10,10 @@ window.themeManager = { void document.documentElement.offsetHeight; // Trigger reflow document.documentElement.style.display = ""; - console.log( - "Theme set. Current attribute:", - document.documentElement.getAttribute("data-bs-theme") - ); + // console.log( + // "Theme set. Current attribute:", + // document.documentElement.getAttribute("data-bs-theme") + // ); }, getTheme: function () { diff --git a/Aquiis.Tests/AccountTests.cs b/Aquiis.Tests/AccountTests.cs new file mode 100644 index 0000000..e3ef356 --- /dev/null +++ b/Aquiis.Tests/AccountTests.cs @@ -0,0 +1,412 @@ +using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; + +namespace Aquiis.Tests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] + +public class AccountManagementTests : PageTest +{ + + private const string BaseUrl = "http://localhost:5197"; + private const int KeepBrowserOpenSeconds = 30; // Set to 0 to close immediately + + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions + { + IgnoreHTTPSErrors = true, + BaseURL = BaseUrl, + RecordVideoDir = Path.Combine(Directory.GetCurrentDirectory(), "test-videos"), + RecordVideoSize = new RecordVideoSize { Width = 1280, Height = 720 } + }; + } + + [Test, Order(1)] + public async Task CreateNewAccount() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Create Account" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).FillAsync("Aquiis"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Confirm Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Register" }).ClickAsync(); + + await Page.WaitForSelectorAsync("h1:has-text('Register confirmation')"); + + // await Page.GetByRole(AriaRole.Heading, new() { Name = "Register confirmation" }).ClickAsync(); + await Page.GetByRole(AriaRole.Link, new() { Name = "Click here to confirm your account" }).ClickAsync(); + + await Page.WaitForSelectorAsync("h1:has-text('Confirm email')"); + + // await Page.GetByText("Thank you for confirming your").ClickAsync(); + await Page.GetByRole(AriaRole.Link, new() { Name = "Home" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + + await Page.WaitForSelectorAsync("h1:has-text('Log in')"); + + // await Page.GetByRole(AriaRole.Heading, new() { Name = "Log in", Exact = true }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForSelectorAsync("text=Dashboard"); + + await Page.GetByRole(AriaRole.Heading, new() { Name = "Property Management Dashboard", Exact= true }).ClickAsync(); + + //Keep browser open for review/recording + // if (KeepBrowserOpenSeconds > 0) + // await Task.Delay(KeepBrowserOpenSeconds * 1000); + } + + [Test, Order(2)] + public async Task AddProperty() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + // Wait for login to complete + await Page.WaitForSelectorAsync("h1:has-text('Property Management Dashboard')"); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Properties" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Add Property" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).FillAsync("369 Crescent Drive"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Apt 2B, Unit" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).FillAsync("New Orleans"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).PressAsync("Tab"); + await Page.Locator("select[name=\"propertyModel.State\"]").SelectOptionAsync(new[] { "LA" }); + await Page.Locator("select[name=\"propertyModel.State\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("70119"); + await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.GetByPlaceholder("0.00").ClickAsync(); + await Page.GetByPlaceholder("0.00").FillAsync("1800"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").FillAsync("2500"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); + + // Verify property was created successfully + await Page.WaitForSelectorAsync("h1:has-text('Properties')"); + await Expect(Page.GetByText("369 Crescent Drive").First).ToBeVisibleAsync(); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Add Property" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).FillAsync("354 Maple Avenue"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Apt 2B, Unit" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).FillAsync("Los Angeles"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).PressAsync("Tab"); + await Page.Locator("select[name=\"propertyModel.State\"]").SelectOptionAsync(new[] { "CA" }); + await Page.Locator("select[name=\"propertyModel.State\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("90210"); + await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.GetByPlaceholder("0.00").ClickAsync(); + await Page.GetByPlaceholder("0.00").FillAsync("4900"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").FillAsync("3200"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); + + // Verify property was created successfully + await Page.WaitForSelectorAsync("h1:has-text('Properties')"); + await Expect(Page.GetByText("354 Maple Avenue").First).ToBeVisibleAsync(); + } + + [Test, Order(3)] + public async Task AddProspect() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Add New Prospect" }).ClickAsync(); + + await Page.Locator("input[name=\"newProspect.FirstName\"]").ClickAsync(); + await Page.Locator("input[name=\"newProspect.FirstName\"]").FillAsync("Mya"); + await Page.Locator("input[name=\"newProspect.FirstName\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.LastName\"]").ClickAsync(); + await Page.Locator("input[name=\"newProspect.LastName\"]").FillAsync("Smith"); + await Page.Locator("input[name=\"newProspect.LastName\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.Email\"]").FillAsync("mya@gmail.com"); + await Page.Locator("input[name=\"newProspect.Email\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.Phone\"]").FillAsync("504-234-3600"); + await Page.Locator("input[name=\"newProspect.Phone\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.DateOfBirth\"]").FillAsync("1993-09-29"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Driver's License #" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Driver's License #" }).FillAsync("12345678"); + await Page.Locator("select[name=\"newProspect.IdentificationState\"]").SelectOptionAsync(new[] { "LA" }); + await Page.Locator("select[name=\"newProspect.Source\"]").SelectOptionAsync(new[] { "Zillow" }); + await Page.Locator("select[name=\"newProspect.InterestedPropertyId\"]").SelectOptionAsync(new[] { "1" }); + await Page.Locator("input[name=\"newProspect.DesiredMoveInDate\"]").FillAsync("2026-01-01"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Save Prospect" }).ClickAsync(); + + // Verify property was created successfully + await Page.WaitForSelectorAsync("h1:has-text('Prospective Tenants')"); + await Expect(Page.GetByText("Mya Smith").First).ToBeVisibleAsync(); + } + + [Test, Order(4)] + public async Task ScheduleAndCompleteTour() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + await Page.GetByTitle("Schedule Tour").ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Schedule Tour" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Complete Tour", Exact = true }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Continue Editing" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).First.ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(1).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(2).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(3).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(4).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(5).ClickAsync(); + + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).First.ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).First.FillAsync("1800"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., $" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., $" }).FillAsync("1800"); + + await Page.Locator("div:nth-child(10) > .card-header > .btn").ClickAsync(); + await Page.Locator("div:nth-child(11) > .card-header > .btn").ClickAsync(); + + await Page.GetByText("Interested", new() { Exact = true }).ClickAsync(); + + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).Nth(1).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).Nth(1).FillAsync("None"); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Save Progress" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Mark as Complete" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Generate PDF" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + //await Task.Delay(5000); // Wait for PDF generation + + var page1 = await Page.RunAndWaitForPopupAsync(async () => + { + await Page.GetByRole(AriaRole.Button, new() { Name = " View PDF" }).ClickAsync(); + }); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + + [Test, Order(5)] + public async Task SubmitApplication() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Apply" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).FillAsync("123 Main Street"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Los Angeles" }).FillAsync("Los Angeles"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Los Angeles" }).PressAsync("Tab"); + await Page.Locator("select[name=\"applicationModel.CurrentState\"]").SelectOptionAsync(new[] { "CA" }); + await Page.Locator("select[name=\"applicationModel.CurrentState\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "90210" }).FillAsync("90210"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "90210" }).PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.CurrentRent\"]").FillAsync("1500"); + await Page.Locator("input[name=\"applicationModel.CurrentRent\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "John Smith" }).FillAsync("John Smith"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "John Smith" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "(555) 123-" }).FillAsync("555-123-4567"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "(555) 123-" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("CapsLock"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).FillAsync("ABC"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("CapsLock"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).FillAsync("ABC Company"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Software Engineer" }).FillAsync("Software Engineer"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Software Engineer" }).PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.MonthlyIncome\"]").FillAsync("9600"); + await Page.Locator("input[name=\"applicationModel.MonthlyIncome\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.EmploymentLengthMonths\"]").FillAsync("15"); + await Page.Locator("input[name=\"applicationModel.EmploymentLengthMonths\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.Reference1Name\"]").FillAsync("Richard"); + await Page.Locator("input[name=\"applicationModel.Reference1Name\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.Reference1Phone\"]").FillAsync("Zachary"); + await Page.Locator("input[name=\"applicationModel.Reference1Phone\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Friend, Coworker, etc." }).FillAsync("Spouse"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Submit Application" }).ClickAsync(); + + // Verify property was created successfully + await Expect(Page.GetByText("Application submitted successfully")).ToBeVisibleAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.WaitForSelectorAsync("h1:has-text('Prospective Tenants')"); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + + [Test, Order(6)] + public async Task ApproveApplication() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByTitle("View Details").ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " View Application" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Collect Application Fee" }).ClickAsync(); + await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new[] { "Online Payment" }); + await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Payment" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Initiate Screening" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Pass" }).First.ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Pass" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Pass" }).Nth(1).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Pass" }).ClickAsync(); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Approve Application" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + + [Test] + public async Task GenerateLeaseOfferAndConvertToLease() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByTitle("View Details").ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " View Application" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Generate Lease Offer" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Generate Lease Offer" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Accept Offer (Convert to Lease" }).ClickAsync(); + + await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new[] { "Online Payment" }); + await Page.GetByRole(AriaRole.Button, new() { Name = " Accept & Create Lease" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + +} \ No newline at end of file diff --git a/Aquiis.Tests/AddProperty.cs b/Aquiis.Tests/ApplicationTests.cs similarity index 60% rename from Aquiis.Tests/AddProperty.cs rename to Aquiis.Tests/ApplicationTests.cs index 489fadf..9db0bb6 100644 --- a/Aquiis.Tests/AddProperty.cs +++ b/Aquiis.Tests/ApplicationTests.cs @@ -1,10 +1,26 @@ using Microsoft.Playwright.NUnit; using Microsoft.Playwright; +namespace Aquiis.Tests; + [Parallelizable(ParallelScope.Self)] [TestFixture] -public class AddProperty : PageTest +public class PropertyManagementTests : PageTest { + private const string BaseUrl = "http://localhost:5197"; + private const int KeepBrowserOpenSeconds = 30; // Set to 0 to close immediately + + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions + { + IgnoreHTTPSErrors = true, + BaseURL = BaseUrl, + RecordVideoDir = Path.Combine(Directory.GetCurrentDirectory(), "test-videos"), + RecordVideoSize = new RecordVideoSize { Width = 1280, Height = 720 } + }; + } + [Test] public async Task CanAddProperty() { @@ -46,4 +62,34 @@ public async Task CanAddProperty() await Page.WaitForSelectorAsync("h1:has-text('Properties')"); await Expect(Page.GetByText("3535 Delaney").First).ToBeVisibleAsync(); } + + [Test] + public async Task CanRemoveProperty() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).PressAsync("Enter"); + + // Wait for login to complete + await Page.WaitForSelectorAsync("text=Dashboard"); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Properties" }).ClickAsync(); + + // Wait for properties page to load + await Page.WaitForSelectorAsync("h1:has-text('Properties')"); + + // Find the property "3535 Delaney" and click its delete button + await Page.Locator("[id^='property-']", new() { HasText = "3535 Delaney" }) + .GetByTitle("Delete").First.ClickAsync(); + + // Confirm deletion + //await Page.GetByRole(AriaRole.Button, new() { Name = "Delete", Exact = true }).ClickAsync(); + + // Verify property was deleted + //await Expect(Page.GetByText("3535 Delaney")).Not.ToBeVisibleAsync(); + } } diff --git a/Aquiis.Tests/RemoveProperty.cs b/Aquiis.Tests/RemoveProperty.cs deleted file mode 100644 index 3662881..0000000 --- a/Aquiis.Tests/RemoveProperty.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.Playwright.NUnit; -using Microsoft.Playwright; - -[Parallelizable(ParallelScope.Self)] -[TestFixture] -public class RemoveProperty : PageTest -{ - [Test] - public async Task CanRemoveProperty() - { - await Page.GotoAsync("http://localhost:5197/"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).PressAsync("Enter"); - - // Wait for login to complete - await Page.WaitForSelectorAsync("text=Dashboard"); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Properties" }).ClickAsync(); - - // Wait for properties page to load - await Page.WaitForSelectorAsync("h1:has-text('Properties')"); - - // Find the property "3535 Delaney" and click its delete button - await Page.Locator("[id^='property-']", new() { HasText = "3535 Delaney" }) - .GetByTitle("Delete").First.ClickAsync(); - - // Confirm deletion - //await Page.GetByRole(AriaRole.Button, new() { Name = "Delete", Exact = true }).ClickAsync(); - - // Verify property was deleted - //await Expect(Page.GetByText("3535 Delaney")).Not.ToBeVisibleAsync(); - } -} \ No newline at end of file diff --git a/Aquiis.Tests/UnitTest1.cs b/Aquiis.Tests/UnitTest1.cs.bak similarity index 99% rename from Aquiis.Tests/UnitTest1.cs rename to Aquiis.Tests/UnitTest1.cs.bak index 0bb1438..e461f17 100644 --- a/Aquiis.Tests/UnitTest1.cs +++ b/Aquiis.Tests/UnitTest1.cs.bak @@ -9,7 +9,7 @@ namespace Aquiis.Tests; /// [Parallelizable(ParallelScope.Self)] [TestFixture] -public class PropertyManagementTests : PageTest +public class PropertyManagementTestExamples : PageTest { private const string BaseUrl = "http://localhost:5197"; private const int KeepBrowserOpenSeconds = 30; // Set to 0 to close immediately From c7503f623e2f97260c279eca19cab34fb6d3d04b Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Tue, 9 Dec 2025 21:03:47 -0600 Subject: [PATCH 09/13] See REVISIONS.md for details --- .gitignore | 4 + .../Services/CalendarEventService.cs | 7 +- .../Services/CalendarSettingsService.cs | 11 +- .../Application/Services/ChecklistService.cs | 60 +- .../Services/FinancialReportService.cs | 10 +- .../Application/Services/NoteService.cs | 16 +- .../Services/OrganizationService.cs | 32 +- .../Services/PropertyManagementService.cs | 203 +- .../Services/ScheduledTaskService.cs | 8 +- .../Services/SecurityDepositService.cs | 38 +- .../Services/TenantConversionService.cs | 8 +- .../Workflows/ApplicationWorkflowService.cs | 52 +- .../Services/Workflows/BaseWorkflowService.cs | 17 +- .../Services/Workflows/WorkflowAuditLog.cs | 7 +- Aquiis.SimpleStart/Aquiis.SimpleStart.csproj | 1 + .../Core/Constants/ApplicationConstants.cs | 41 +- .../Core/Constants/EntityTypeNames.cs | 65 + .../Core/Entities/ApplicationScreening.cs | 10 +- Aquiis.SimpleStart/Core/Entities/BaseModel.cs | 4 +- .../Core/Entities/CalendarEvent.cs | 10 +- .../Core/Entities/CalendarSettings.cs | 6 +- Aquiis.SimpleStart/Core/Entities/Checklist.cs | 16 +- .../Core/Entities/ChecklistItem.cs | 10 +- .../Core/Entities/ChecklistTemplate.cs | 6 +- .../Core/Entities/ChecklistTemplateItem.cs | 10 +- Aquiis.SimpleStart/Core/Entities/Document.cs | 12 +- .../Core/Entities/ISchedulableEntity.cs | 8 +- .../Core/Entities/IncomeStatement.cs | 18 +- .../Core/Entities/Inspection.cs | 12 +- Aquiis.SimpleStart/Core/Entities/Invoice.cs | 6 +- Aquiis.SimpleStart/Core/Entities/Lease.cs | 20 +- .../Core/Entities/LeaseOffer.cs | 10 +- .../Core/Entities/MaintenanceRequest.cs | 16 +- Aquiis.SimpleStart/Core/Entities/Note.cs | 4 +- .../Core/Entities/Organization.cs | 6 +- .../Core/Entities/OrganizationSettings.cs | 2 +- Aquiis.SimpleStart/Core/Entities/Payment.cs | 6 +- Aquiis.SimpleStart/Core/Entities/Property.cs | 2 +- .../Core/Entities/ProspectiveTenant.cs | 4 +- .../Core/Entities/RentalApplication.cs | 6 +- .../Core/Entities/SecurityDeposit.cs | 6 +- .../Core/Entities/SecurityDepositDividend.cs | 10 +- .../Entities/SecurityDepositInvestmentPool.cs | 2 +- Aquiis.SimpleStart/Core/Entities/Tenant.cs | 7 +- Aquiis.SimpleStart/Core/Entities/Tour.cs | 20 +- .../Core/Entities/UserOrganization.cs | 9 +- .../Core/Validation/OptionalGuidAttribute.cs | 73 + .../Core/Validation/RequiredGuidAttribute.cs | 83 + .../Pages/EditOrganization.razor | 4 +- .../Organizations/Pages/ManageUsers.razor | 6 +- .../Organizations/Pages/Organizations.razor | 6 +- .../Pages/ViewOrganization.razor | 4 +- .../Settings/Pages/CalendarSettings.razor | 10 +- .../Settings/Pages/OrganizationSettings.razor | 5 +- .../Settings/Pages/ServiceSettings.razor | 30 +- .../Administration/Users/Manage.razor | 4 +- .../Administration/Users/Pages/Create.razor | 8 +- .../Features/Administration/Users/View.razor | 12 +- .../Applications/Pages/Applications.razor | 6 +- .../Pages/GenerateLeaseOffer.razor | 8 +- .../Pages/ProspectiveTenants.razor | 26 +- .../Pages/ReviewApplication.razor | 8 +- .../Applications/Pages/ScheduleTour.razor | 24 +- .../Pages/SubmitApplication.razor | 20 +- .../Applications/Pages/Tours.razor | 14 +- .../Applications/Pages/ToursCalendar.razor | 10 +- .../Pages/ViewProspectiveTenant.razor | 10 +- .../PropertyManagement/Calendar.razor | 46 +- .../PropertyManagement/CalendarListView.razor | 32 +- .../Checklists/Pages/Checklists.razor | 2 +- .../Checklists/Pages/Complete.razor | 64 +- .../Checklists/Pages/Create.razor | 16 +- .../Checklists/Pages/EditTemplate.razor | 12 +- .../Checklists/Pages/MyChecklists.razor | 4 +- .../Checklists/Pages/Templates.razor | 4 +- .../Checklists/Pages/View.razor | 7 +- .../Documents/Pages/Documents.razor | 18 +- .../Documents/Pages/LeaseDocuments.razor | 6 +- .../Inspections/Pages/Create.razor | 14 +- .../Inspections/Pages/Schedule.razor | 4 +- .../Inspections/Pages/View.razor | 6 +- .../Invoices/Pages/CreateInvoice.razor | 10 +- .../Invoices/Pages/EditInvoice.razor | 9 +- .../Invoices/Pages/Invoices.razor | 20 +- .../Invoices/Pages/ViewInvoice.razor | 4 +- .../LeaseOffers/Pages/LeaseOffers.razor | 8 +- .../LeaseOffers/Pages/ViewLeaseOffer.razor | 8 +- .../Leases/Pages/AcceptLease.razor | 10 +- .../Leases/Pages/CreateLease.razor | 17 +- .../Leases/Pages/EditLease.razor | 14 +- .../Leases/Pages/Leases.razor | 26 +- .../Leases/Pages/ViewLease.razor | 35 +- .../Pages/CreateMaintenanceRequest.razor | 22 +- .../Pages/EditMaintenanceRequest.razor | 8 +- .../Pages/MaintenanceRequests.razor | 6 +- .../Pages/ViewMaintenanceRequest.razor | 4 +- .../Payments/Pages/CreatePayment.razor | 7 +- .../Payments/Pages/EditPayment.razor | 4 +- .../Payments/Pages/Payments.razor | 10 +- .../Payments/Pages/ViewPayment.razor | 4 +- .../Properties/Pages/Edit.razor | 4 +- .../Properties/Pages/Index.razor | 6 +- .../Properties/Pages/View.razor | 12 +- .../Reports/Pages/IncomeStatementReport.razor | 16 +- .../Pages/PropertyPerformanceReport.razor | 9 +- .../Reports/Pages/RentRollReport.razor | 6 +- .../Reports/Pages/TaxReport.razor | 13 +- .../Pages/CalculateDividends.razor | 8 +- .../Pages/InvestmentPools.razor | 6 +- .../Pages/SecurityDeposits.razor | 6 +- .../Pages/ViewInvestmentPool.razor | 4 +- .../Tenants/Pages/EditTenant.razor | 4 +- .../Tenants/Pages/Tenants.razor | 6 +- .../Tenants/Pages/View.razor | 5 +- .../Data/ApplicationDbContext.cs | 86 +- .../20251204004153_InitialCreate.Designer.cs | 3886 ---------------- ...1206163651_AddWorkflowAuditLog.Designer.cs | 3963 ----------------- .../20251206163651_AddWorkflowAuditLog.cs | 84 - ...eWorkflowAuditLogOrganizationIdToString.cs | 34 - ... 20251209234246_InitialCreate.Designer.cs} | 626 ++- ...ate.cs => 20251209234246_InitialCreate.cs} | 364 +- .../ApplicationDbContextModelSnapshot.cs | 622 ++- .../Components/Account/ApplicationUser.cs | 4 +- .../Shared/Components/NotesTimeline.razor | 31 +- .../Components/OrganizationSwitcher.razor | 2 +- .../Shared/Components/Pages/Home.razor | 98 +- .../Shared/Services/DocumentService.cs | 4 +- .../Shared/Services/UserContextService.cs | 24 +- Aquiis.Tests/AccountTests.cs | 10 +- Aquiis.Tests/Aquiis.Tests.csproj | 4 + .../GuidValidationAttributeTests.cs | 200 + 131 files changed, 2184 insertions(+), 9633 deletions(-) create mode 100644 Aquiis.SimpleStart/Core/Constants/EntityTypeNames.cs create mode 100644 Aquiis.SimpleStart/Core/Validation/OptionalGuidAttribute.cs create mode 100644 Aquiis.SimpleStart/Core/Validation/RequiredGuidAttribute.cs delete mode 100644 Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251204004153_InitialCreate.Designer.cs delete mode 100644 Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206163651_AddWorkflowAuditLog.Designer.cs delete mode 100644 Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206163651_AddWorkflowAuditLog.cs delete mode 100644 Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.cs rename Aquiis.SimpleStart/Infrastructure/Data/Migrations/{20251206182946_ChangeWorkflowAuditLogOrganizationIdToString.Designer.cs => 20251209234246_InitialCreate.Designer.cs} (87%) rename Aquiis.SimpleStart/Infrastructure/Data/Migrations/{20251204004153_InitialCreate.cs => 20251209234246_InitialCreate.cs} (81%) create mode 100644 Aquiis.Tests/Core/Validation/GuidValidationAttributeTests.cs 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/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 13b6948..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,7 @@ public async Task> GetLeasesByPropertyIdAsync(int propertyId) .ToList(); } - public async Task> GetCurrentAndUpcomingLeasesByPropertyIdAsync(int propertyId) + public async Task> GetCurrentAndUpcomingLeasesByPropertyIdAsync(Guid propertyId) { var _userId = await _userContext.GetUserIdAsync(); @@ -521,11 +523,12 @@ public async Task> GetCurrentAndUpcomingLeasesByPropertyIdAsync(int .Where(l => l.PropertyId == propertyId && !l.IsDeleted && l.Property.OrganizationId == organizationId - && (l.Status == "Active" || l.Status == "Pending")) + && (l.Status == ApplicationConstants.LeaseStatuses.Pending + || l.Status == ApplicationConstants.LeaseStatuses.Active)) .ToListAsync(); } - public async Task> GetActiveLeasesByPropertyIdAsync(int propertyId) + public async Task> GetActiveLeasesByPropertyIdAsync(Guid propertyId) { var _userId = await _userContext.GetUserIdAsync(); @@ -549,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(); @@ -585,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; @@ -631,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(); @@ -664,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(); @@ -706,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(); @@ -722,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(); @@ -760,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; @@ -792,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(); @@ -823,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}"; } @@ -852,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 @@ -865,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 @@ -887,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; @@ -919,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; @@ -959,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(); @@ -1008,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(); @@ -1025,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(); @@ -1039,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(); @@ -1052,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(); @@ -1075,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(); @@ -1105,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(); @@ -1206,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(); @@ -1219,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(); @@ -1240,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); @@ -1295,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(); @@ -1304,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."); } @@ -1341,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(); @@ -1414,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(); @@ -1458,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, @@ -1495,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(); @@ -1507,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(); @@ -1586,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(); @@ -1606,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; @@ -1640,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(); @@ -1649,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(); @@ -1677,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(); @@ -1719,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"); } @@ -1734,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, @@ -1753,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) @@ -1768,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"); } @@ -1802,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 @@ -1818,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; @@ -1852,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(); @@ -1896,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 @@ -1908,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 @@ -1919,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; @@ -1938,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 @@ -2013,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(); @@ -2024,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."); } @@ -2056,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(); @@ -2067,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."); } @@ -2104,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(); @@ -2112,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."); } @@ -2143,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(); @@ -2151,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."); } @@ -2193,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 @@ -2204,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 @@ -2219,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; @@ -2304,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(); @@ -2337,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 @@ -2350,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; @@ -2402,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(); @@ -2413,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; @@ -2447,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(); @@ -2477,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; @@ -2585,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); @@ -2593,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 @@ -2603,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 @@ -2613,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 @@ -2648,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 02e9f6e..5d0ac1c 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 () => @@ -122,6 +126,7 @@ public async Task> SubmitApplicationAsync( // Create application var application = new RentalApplication { + Id = Guid.NewGuid(), OrganizationId = orgId, ProspectiveTenantId = prospectId, PropertyId = propertyId, @@ -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) { @@ -343,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 () => { @@ -399,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 () => { @@ -460,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 () => { @@ -524,7 +529,7 @@ await LogTransitionAsync( /// Does not automatically approve - requires manual ApproveApplicationAsync call. /// public async Task CompleteScreeningAsync( - int applicationId, + Guid applicationId, ScreeningResultModel results) { return await ExecuteWorkflowAsync(async () => @@ -583,7 +588,7 @@ await LogTransitionAsync( /// Creates LeaseOffer entity, updates property to LeasePending, and denies competing applications. /// public async Task> GenerateLeaseOfferAsync( - int applicationId, + Guid applicationId, LeaseOfferModel model) { return await ExecuteWorkflowAsync(async () => @@ -621,6 +626,7 @@ public async Task> GenerateLeaseOfferAsync( // Create lease offer var leaseOffer = new LeaseOffer { + Id = Guid.NewGuid(), OrganizationId = orgId, RentalApplicationId = applicationId, PropertyId = property.Id, @@ -725,7 +731,7 @@ await LogTransitionAsync( /// Records security deposit payment. /// public async Task> AcceptLeaseOfferAsync( - int leaseOfferId, + Guid leaseOfferId, string depositPaymentMethod, DateTime depositPaymentDate, string? depositReferenceNumber = null, @@ -760,6 +766,7 @@ public async Task> AcceptLeaseOfferAsync( // Convert prospect to tenant var tenant = new Tenant { + Id = Guid.NewGuid(), OrganizationId = orgId, FirstName = prospect.FirstName, LastName = prospect.LastName, @@ -779,6 +786,7 @@ public async Task> AcceptLeaseOfferAsync( // Create lease var lease = new Lease { + Id = Guid.NewGuid(), OrganizationId = orgId, PropertyId = leaseOffer.PropertyId, Tenant = tenant, // Use navigation property instead of TenantId @@ -788,7 +796,7 @@ public async Task> AcceptLeaseOfferAsync( MonthlyRent = leaseOffer.MonthlyRent, SecurityDeposit = leaseOffer.SecurityDeposit, Terms = leaseOffer.Terms, - Status = "Active", + Status = ApplicationConstants.LeaseStatuses.Active, SignedOn = DateTime.UtcNow, CreatedBy = userId, CreatedOn = DateTime.UtcNow @@ -800,6 +808,7 @@ public async Task> AcceptLeaseOfferAsync( // Create security deposit record var securityDeposit = new SecurityDeposit { + Id = Guid.NewGuid(), OrganizationId = orgId, Lease = lease, // Use navigation property Tenant = tenant, // Use navigation property @@ -861,6 +870,13 @@ await LogTransitionAsync( 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"); }); @@ -870,7 +886,7 @@ await LogTransitionAsync( /// Declines a lease offer. /// Rolls back property status and marks prospect as lease declined. /// - public async Task DeclineLeaseOfferAsync(int leaseOfferId, string declineReason) + public async Task DeclineLeaseOfferAsync(Guid leaseOfferId, string declineReason) { return await ExecuteWorkflowAsync(async () => { @@ -938,7 +954,7 @@ await LogTransitionAsync( /// Expires a lease offer (called by scheduled task). /// Similar to decline but automated. /// - public async Task ExpireLeaseOfferAsync(int leaseOfferId) + public async Task ExpireLeaseOfferAsync(Guid leaseOfferId) { return await ExecuteWorkflowAsync(async () => { @@ -1005,7 +1021,7 @@ await LogTransitionAsync( #region Helper Methods - private async Task GetApplicationAsync(int applicationId) + private async Task GetApplicationAsync(Guid applicationId) { var orgId = await GetActiveOrganizationIdAsync(); return await _context.RentalApplications @@ -1019,8 +1035,8 @@ await LogTransitionAsync( } private async Task ValidateApplicationSubmissionAsync( - int prospectId, - int propertyId) + Guid prospectId, + Guid propertyId) { var errors = new List(); var orgId = await GetActiveOrganizationIdAsync(); @@ -1069,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(); diff --git a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs index 491f66d..669f696 100644 --- a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs +++ b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs @@ -122,7 +122,7 @@ protected async Task ExecuteWorkflowAsync( /// protected async Task LogTransitionAsync( string entityType, - int entityId, + Guid entityId, string? fromStatus, string toStatus, string action, @@ -134,6 +134,7 @@ protected async Task LogTransitionAsync( var auditLog = new WorkflowAuditLog { + Id = Guid.NewGuid(), EntityType = entityType, EntityId = entityId, FromStatus = fromStatus, @@ -142,7 +143,7 @@ protected async Task LogTransitionAsync( Reason = reason, PerformedBy = userId, PerformedOn = DateTime.UtcNow, - OrganizationId = activeOrgId ?? string.Empty, + OrganizationId = activeOrgId.HasValue ? activeOrgId.Value : Guid.Empty, Metadata = metadata != null ? JsonSerializer.Serialize(metadata) : null, CreatedOn = DateTime.UtcNow, CreatedBy = userId @@ -157,7 +158,7 @@ protected async Task LogTransitionAsync( /// public async Task> GetAuditHistoryAsync( string entityType, - int entityId) + Guid entityId) { var activeOrgId = await _userContext.GetActiveOrganizationIdAsync(); @@ -173,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") == activeOrgId) + .Where(e => EF.Property(e, "Id") == entityId) + .Where(e => EF.Property(e, "OrganizationId") == activeOrgId) .Where(e => EF.Property(e, "IsDeleted") == false) .FirstOrDefaultAsync(); @@ -199,9 +200,9 @@ protected async Task GetCurrentUserIdAsync() /// /// Gets the active organization ID from the user context. /// - protected async Task GetActiveOrganizationIdAsync() + protected async Task GetActiveOrganizationIdAsync() { - return await _userContext.GetActiveOrganizationIdAsync() ?? string.Empty; + 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 8cd6df4..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 string 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 0dcb7b8..1556caf 100644 --- a/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs +++ b/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs @@ -235,6 +235,8 @@ 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"; @@ -256,6 +258,8 @@ public static class LeaseStatuses { { Offered, Pending, + Accepted, + AcceptedPendingStart, Active, Declined, Renewed, @@ -297,10 +301,18 @@ public static class PropertyStatuses public const string Available = "Available"; public const string ApplicationPending = "Application Pending"; public const string LeasePending = "Lease Pending"; + public const string MoveInPending = "Accepted - Move-In Pending"; public const string Occupied = "Occupied"; + public const string MoveOutPending = "Move-Out Pending"; public const string UnderRenovation = "Under Renovation"; public const string OffMarket = "Off Market"; + public static IReadOnlyList OccupiedStatuses { get; } = new List + { + MoveInPending, + Occupied, + MoveOutPending + }; public static IReadOnlyList AllPropertyStatuses { get; } = new List { Available, @@ -373,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 }; @@ -635,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 b7723be..0379e99 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor @@ -1,4 +1,4 @@ -@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 @@ -261,7 +261,7 @@ @code { [Parameter] - public int ApplicationId { get; set; } + public Guid ApplicationId { get; set; } private RentalApplication? application; private LeaseOfferModel leaseModel = new(); @@ -269,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(); } 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 2089fca..ae1e428 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor @@ -1,4 +1,4 @@ -@page "/propertymanagement/applications/{ApplicationId:int}/review" +@page "/propertymanagement/applications/{ApplicationId:guid}/review" @using Aquiis.SimpleStart.Core.Entities @using Aquiis.SimpleStart.Application.Services @@ -766,7 +766,7 @@ @code { [Parameter] - public int ApplicationId { get; set; } + public Guid ApplicationId { get; set; } private RentalApplication? application; private ApplicationScreening? screening; @@ -786,7 +786,7 @@ private string denyReason = string.Empty; private string withdrawReason = string.Empty; private string userId = string.Empty; - private string organizationId = string.Empty; + private Guid organizationId = Guid.Empty; private FeePaymentModel feePaymentModel = new(); private ScreeningDispositionModel backgroundCheckModel = new(); private ScreeningDispositionModel creditCheckModel = new(); @@ -797,7 +797,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 LoadApplication(); } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor index caf0d12..951c474 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor @@ -1,4 +1,4 @@ -@page "/PropertyManagement/Tours/Schedule/{ProspectId:int}" +@page "/PropertyManagement/Tours/Schedule/{ProspectId:guid}" @using Aquiis.SimpleStart.Core.Entities @using Aquiis.SimpleStart.Application.Services @@ -182,7 +182,7 @@ @code { [Parameter] - public int ProspectId { get; set; } + public Guid ProspectId { get; set; } private ProspectiveTenant? prospect; private List availableProperties = new(); @@ -202,7 +202,7 @@ try { var organizationId = await UserContext.GetActiveOrganizationIdAsync(); - if (!string.IsNullOrEmpty(organizationId)) + if (organizationId.HasValue) { prospect = await PropertyService.GetProspectiveTenantByIdAsync(ProspectId); @@ -233,10 +233,10 @@ newTour = new TourViewModel { ProspectiveTenantId = ProspectId, - PropertyId = prospect.InterestedPropertyId ?? 0, + PropertyId = prospect.InterestedPropertyId ?? Guid.Empty, ScheduledOn = DateTime.Now.AddDays(1).Date.AddHours(10), // Default to tomorrow at 10 AM DurationMinutes = 30, - ChecklistTemplateId = tourTemplates.FirstOrDefault(t => t.IsSystemTemplate && t.Name == "Property Tour")?.Id ?? tourTemplates.FirstOrDefault()?.Id ?? 0 + ChecklistTemplateId = tourTemplates.FirstOrDefault(t => t.IsSystemTemplate && t.Name == "Property Tour")?.Id ?? tourTemplates.FirstOrDefault()?.Id }; } } @@ -255,13 +255,13 @@ { try { - if (newTour.PropertyId == 0) + if (newTour.PropertyId == Guid.Empty) { ToastService.ShowError("Please select a property"); return; } - if (newTour.ChecklistTemplateId == 0) + if (!newTour.ChecklistTemplateId.HasValue || newTour.ChecklistTemplateId.Value == Guid.Empty) { ToastService.ShowError("Please select a checklist template"); return; @@ -270,7 +270,7 @@ 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("User context not available"); return; @@ -283,7 +283,7 @@ PropertyId = newTour.PropertyId, ScheduledOn = newTour.ScheduledOn, DurationMinutes = newTour.DurationMinutes, - OrganizationId = organizationId, + OrganizationId = organizationId.Value, CreatedBy = userId }; @@ -306,10 +306,10 @@ public class TourViewModel { [Required] - public int ProspectiveTenantId { get; set; } + public Guid ProspectiveTenantId { get; set; } [Required(ErrorMessage = "Property is required")] - public int PropertyId { get; set; } + public Guid PropertyId { get; set; } [Required(ErrorMessage = "Date and time is required")] public DateTime ScheduledOn { get; set; } @@ -319,6 +319,6 @@ public int DurationMinutes { get; set; } [Required(ErrorMessage = "Checklist template is required")] - public int ChecklistTemplateId { get; set; } + public Guid? ChecklistTemplateId { get; set; } } } diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor index 2de420c..19e4362 100644 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor +++ b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor @@ -1,6 +1,7 @@ -@page "/propertymanagement/prospects/{ProspectId:int}/submit-application" +@page "/propertymanagement/prospects/{ProspectId:guid}/submit-application" @using Aquiis.SimpleStart.Core.Entities +@using Aquiis.SimpleStart.Core.Validation @using Aquiis.SimpleStart.Application.Services @using Aquiis.SimpleStart.Shared.Services @using Aquiis.SimpleStart.Application.Services.PdfGenerators @@ -362,7 +363,7 @@ @code { [Parameter] - public int ProspectId { get; set; } + public Guid ProspectId { get; set; } private ProspectiveTenant? prospect; private RentalApplication? existingApplication; @@ -375,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() { @@ -383,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(); } @@ -431,7 +432,7 @@ private void UpdateSelectedProperty() { - if (applicationModel.PropertyId > 0) + if (applicationModel.PropertyId != Guid.Empty) { selectedProperty = availableProperties.FirstOrDefault(p => p.Id == applicationModel.PropertyId); } @@ -443,14 +444,14 @@ 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 = 0; + applicationModel.PropertyId = Guid.Empty; selectedProperty = null; } StateHasChanged(); @@ -541,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 b30ed8e..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 } - @@ -327,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; @@ -338,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(); @@ -381,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 }; @@ -393,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 @@ -456,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; } } @@ -476,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) @@ -621,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 { @@ -676,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 @@