Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"

- name: Restore
run: dotnet restore Aquiis.sln

- name: Build
run: dotnet build Aquiis.sln --no-restore --configuration Release

- name: Run focused tests
run: dotnet test Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj --no-build --verbosity normal
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ Data/Backups/**

/Data/app*


# Python virtual environment created per-project
.venv/

Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using System.Security.Claims;
using System.Threading.Tasks;
using System;
using Aquiis.SimpleStart.Application.Services.Workflows;
using Aquiis.SimpleStart.Core.Entities;
using Aquiis.SimpleStart.Shared.Services;
using Aquiis.SimpleStart.Shared.Components.Account;
using Aquiis.SimpleStart.Core.Constants;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;

namespace Aquiis.SimpleStart.Tests;

public class ApplicationWorkflowServiceLeaseLifecycleTests
{
[Fact]
public async Task GenerateAndAcceptLeaseOffer_CreatesLeaseAndTenant_UpdatesProperty()
{
// Arrange - setup SQLite in-memory
var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<Infrastructure.Data.ApplicationDbContext>()
.UseSqlite(connection)
.Options;

var testUserId = "test-user-id";
var orgId = Guid.NewGuid();

// Mock AuthenticationStateProvider
var claims = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, testUserId) }, "TestAuth"));
var mockAuth = new Mock<AuthenticationStateProvider>();
mockAuth.Setup(a => a.GetAuthenticationStateAsync())
.ReturnsAsync(new AuthenticationState(claims));

var mockUserStore = new Mock<IUserStore<ApplicationUser>>();
var mockUserManager = new Mock<UserManager<ApplicationUser>>(
mockUserStore.Object,
null, null, null, null, null, null, null, null);

var appUser = new ApplicationUser { Id = testUserId, ActiveOrganizationId = orgId };
mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny<string>())).ReturnsAsync(appUser);

var serviceProvider = new Mock<IServiceProvider>();

var userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object);

// Create DbContext and seed data
await using var context = new Infrastructure.Data.ApplicationDbContext(options);
await context.Database.EnsureCreatedAsync();

var appUserEntity = new ApplicationUser { Id = testUserId, UserName = "testuser", Email = "t@t.com", ActiveOrganizationId = orgId };
context.Users.Add(appUserEntity);

var org = new Organization { Id = orgId, Name = "TestOrg", OwnerId = testUserId, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow };
context.Organizations.Add(org);

var prospect = new ProspectiveTenant { Id = Guid.NewGuid(), OrganizationId = orgId, FirstName = "Lease", LastName = "Tester", Email = "lt@example.com", Phone = "123", Status = ApplicationConstants.ProspectiveStatuses.Lead, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow };
var property = new Property { Id = Guid.NewGuid(), OrganizationId = orgId, Address = "456 Elm", City = "X", State = "ST", ZipCode = "00000", Status = ApplicationConstants.PropertyStatuses.Available, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow, MonthlyRent = 1200m };
context.ProspectiveTenants.Add(prospect);
context.Properties.Add(property);
await context.SaveChangesAsync();

var noteService = new Application.Services.NoteService(context, userContext);
var workflowService = new ApplicationWorkflowService(context, userContext, noteService);

// Submit application
var submissionModel = new ApplicationSubmissionModel
{
ApplicationFee = 25m,
ApplicationFeePaid = true,
ApplicationFeePaymentMethod = "Card",
CurrentAddress = "Addr",
CurrentCity = "C",
CurrentState = "ST",
CurrentZipCode = "00000",
CurrentRent = 1000m,
LandlordName = "L",
LandlordPhone = "P",
EmployerName = "E",
JobTitle = "J",
MonthlyIncome = 2000m,
EmploymentLengthMonths = 12,
Reference1Name = "R1",
Reference1Phone = "111",
Reference1Relationship = "Friend"
};

var submitResult = await workflowService.SubmitApplicationAsync(prospect.Id, property.Id, submissionModel);
Assert.True(submitResult.Success, string.Join(";", submitResult.Errors));
var application = submitResult.Data!;

// Initiate screening and complete it as Passed
var screeningResult = await workflowService.InitiateScreeningAsync(application.Id, true, true);
Assert.True(screeningResult.Success, string.Join(";", screeningResult.Errors));

var completeScreeningResult = await workflowService.CompleteScreeningAsync(application.Id, new ScreeningResultModel
{
BackgroundCheckPassed = true,
CreditCheckPassed = true,
CreditScore = 700,
OverallResult = "Passed",
ResultNotes = "All good"
});
Assert.True(completeScreeningResult.Success, string.Join(";", completeScreeningResult.Errors));

// Approve application
var approveResult = await workflowService.ApproveApplicationAsync(application.Id);
Assert.True(approveResult.Success, string.Join(";", approveResult.Errors));

// Generate lease offer
var offerModel = new LeaseOfferModel
{
StartDate = DateTime.Today.AddDays(14),
EndDate = DateTime.Today.AddYears(1).AddDays(14),
MonthlyRent = property.MonthlyRent,
SecurityDeposit = property.MonthlyRent,
Terms = "Standard",
Notes = "Test offer"
};

var generateResult = await workflowService.GenerateLeaseOfferAsync(application.Id, offerModel);
Assert.True(generateResult.Success, string.Join(";", generateResult.Errors));
var leaseOffer = generateResult.Data!;

// Accept lease offer
var acceptResult = await workflowService.AcceptLeaseOfferAsync(leaseOffer.Id, "Card", DateTime.UtcNow);
Assert.True(acceptResult.Success, string.Join(";", acceptResult.Errors));
var lease = acceptResult.Data!;

// Assert: Lease exists in DB, Tenant created, Property status Occupied
var dbLease = await context.Leases.Include(l => l.Tenant).FirstOrDefaultAsync(l => l.Id == lease.Id);
Assert.NotNull(dbLease);
Assert.NotNull(dbLease!.Tenant);

var dbProperty = await context.Properties.FirstOrDefaultAsync(p => p.Id == property.Id);
Assert.NotNull(dbProperty);
Assert.Equal(ApplicationConstants.PropertyStatuses.Occupied, dbProperty!.Status);

// Audit logs should contain LeaseOffer Accept entry
var audit = await context.WorkflowAuditLogs.FirstOrDefaultAsync(w => w.EntityType == "LeaseOffer" && w.EntityId == leaseOffer.Id);
Assert.NotNull(audit);
}
}
116 changes: 116 additions & 0 deletions Aquiis.SimpleStart.Tests/ApplicationWorkflowServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System.Security.Claims;
using System.Threading.Tasks;
using System;
using Aquiis.SimpleStart.Application.Services.Workflows;
using Aquiis.SimpleStart.Core.Entities;
using Aquiis.SimpleStart.Shared.Services;
using Aquiis.SimpleStart.Shared.Components.Account;
using Aquiis.SimpleStart.Core.Constants;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;

namespace Aquiis.SimpleStart.Tests;

public class ApplicationWorkflowServiceTests
{
[Fact]
public async Task GetApplicationWorkflowStateAsync_ReturnsExpectedState()
{
// Arrange
// Use SQLite in-memory to support transactions used by workflow base class
var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<Infrastructure.Data.ApplicationDbContext>()
.UseSqlite(connection)
.Options;
// Create test user and org
var testUserId = "test-user-id";
var orgId = Guid.NewGuid();

// Mock AuthenticationStateProvider
var claims = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, testUserId) }, "TestAuth"));
var mockAuth = new Mock<AuthenticationStateProvider>();
mockAuth.Setup(a => a.GetAuthenticationStateAsync())
.ReturnsAsync(new AuthenticationState(claims));

// Mock UserManager to return an ApplicationUser with ActiveOrganizationId set
var mockUserStore = new Mock<IUserStore<ApplicationUser>>();
var mockUserManager = new Mock<UserManager<ApplicationUser>>(
mockUserStore.Object,
null, null, null, null, null, null, null, null);

var appUser = new ApplicationUser { Id = testUserId, ActiveOrganizationId = orgId };
mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny<string>())).ReturnsAsync(appUser);

var serviceProvider = new Mock<IServiceProvider>();

// Create real UserContextService using mocks
var userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object);

// Create DbContext and seed prospect/property
await using var context = new Infrastructure.Data.ApplicationDbContext(options);
// Ensure schema is created for SQLite in-memory
await context.Database.EnsureCreatedAsync();
var appUserEntity = new ApplicationUser { Id = testUserId, UserName = "testuser", Email = "t@t.com", ActiveOrganizationId = orgId };
context.Users.Add(appUserEntity);

var org = new Organization { Id = orgId, Name = "TestOrg", OwnerId = testUserId, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow };
context.Organizations.Add(org);

var prospect = new ProspectiveTenant { Id = Guid.NewGuid(), OrganizationId = orgId, FirstName = "Test", LastName = "User", Email = "t@t.com", Phone = "123", Status = "Lead", CreatedBy = testUserId, CreatedOn = DateTime.UtcNow };
var property = new Property { Id = Guid.NewGuid(), OrganizationId = orgId, Address = "123 Main", City = "X", State = "ST", ZipCode = "00000", Status = ApplicationConstants.PropertyStatuses.Available, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow };
context.ProspectiveTenants.Add(prospect);
context.Properties.Add(property);
await context.SaveChangesAsync();

// Create NoteService (not used heavily in this test)
var noteService = new Application.Services.NoteService(context, userContext);

var workflowService = new ApplicationWorkflowService(context, userContext, noteService);

// Act - submit application then initiate screening
var submissionModel = new ApplicationSubmissionModel
{
ApplicationFee = 25m,
ApplicationFeePaid = true,
ApplicationFeePaymentMethod = "Card",
CurrentAddress = "Addr",
CurrentCity = "C",
CurrentState = "ST",
CurrentZipCode = "00000",
CurrentRent = 1000m,
LandlordName = "L",
LandlordPhone = "P",
EmployerName = "E",
JobTitle = "J",
MonthlyIncome = 2000m,
EmploymentLengthMonths = 12,
Reference1Name = "R1",
Reference1Phone = "111",
Reference1Relationship = "Friend"
};

var submitResult = await workflowService.SubmitApplicationAsync(prospect.Id, property.Id, submissionModel);
Assert.True(submitResult.Success, string.Join(";", submitResult.Errors));

var application = submitResult.Data!;

var screeningResult = await workflowService.InitiateScreeningAsync(application.Id, true, true);
Assert.True(screeningResult.Success, string.Join(";", screeningResult.Errors));

// Get aggregated workflow state
var state = await workflowService.GetApplicationWorkflowStateAsync(application.Id);

// Assert
Assert.NotNull(state.Application);
Assert.NotNull(state.Prospect);
Assert.NotNull(state.Property);
Assert.NotNull(state.Screening);
Assert.NotEmpty(state.AuditHistory);

}
}
21 changes: 21 additions & 0 deletions Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.6.2" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.11" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aquiis.SimpleStart\Aquiis.SimpleStart.csproj" />
</ItemGroup>

</Project>
38 changes: 38 additions & 0 deletions Aquiis.SimpleStart/.github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading