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
940 changes: 940 additions & 0 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

55 changes: 52 additions & 3 deletions 0-Aquiis.Core/Constants/ApplicationConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static class OrganizationRoles
/// <summary>
/// PropertyManager - Full property management features (no admin/settings access)
/// </summary>
public const string PropertyManager = "Property Manager";
public const string PropertyManager = "PropertyManager";

/// <summary>
/// Maintenance - Maintenance requests, work orders, and vendors
Expand Down Expand Up @@ -176,14 +176,16 @@ public static class InvoiceStatuses
public const string Paid = "Paid";
public const string Overdue = "Overdue";
public const string Cancelled = "Cancelled";
public const string Voided = "Voided";

public static IReadOnlyList<string> AllInvoiceStatuses { get; } = new List<string>
{
Pending,
PaidPartial,
Paid,
Overdue,
Cancelled
Cancelled,
Voided
};
}

Expand All @@ -193,13 +195,15 @@ public static class PaymentStatuses
public const string Pending = "Pending";
public const string Failed = "Failed";
public const string Refunded = "Refunded";
public const string Voided = "Voided";

public static IReadOnlyList<string> AllPaymentStatuses { get; } = new List<string>
{
Completed,
Pending,
Failed,
Refunded
Refunded,
Voided
};
}
public static class InspectionTypes
Expand Down Expand Up @@ -275,6 +279,13 @@ public static class LeaseStatuses {
Terminated,
Expired
};

public static IReadOnlyList<string> DefaultStatuses { get; } = new List<string>
{
Active,
MonthToMonth,
Terminated
};
}


Expand Down Expand Up @@ -359,6 +370,44 @@ public static class MaintenanceRequestTypes
};
}

/// <summary>
/// Repair type categories for logging work performed on properties.
/// Used by the Repair entity (work WITHOUT workflow).
/// </summary>
public static class RepairTypes
{
public const string Plumbing = "Plumbing";
public const string Electrical = "Electrical";
public const string HeatingCooling = "Heating/Cooling";
public const string Appliance = "Appliance";
public const string Structural = "Structural";
public const string Landscaping = "Landscaping";
public const string PestControl = "Pest Control";
public const string Painting = "Painting";
public const string Carpentry = "Carpentry";
public const string Flooring = "Flooring";
public const string Roofing = "Roofing";
public const string Windows = "Windows/Doors";
public const string Other = "Other";

public static IReadOnlyList<string> AllRepairTypes { get; } = new List<string>
{
Plumbing,
Electrical,
HeatingCooling,
Appliance,
Structural,
Landscaping,
PestControl,
Painting,
Carpentry,
Flooring,
Roofing,
Windows,
Other
};
}

public static class MaintenanceRequestPriorities
{
public const string Low = "Low";
Expand Down
1 change: 1 addition & 0 deletions 0-Aquiis.Core/Constants/ApplicationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class ApplicationSettings
public string Repository { get; set; } = string.Empty;
public bool SoftDeleteEnabled { get; set; }
public string SchemaVersion { get; set; } = "1.0.0";
public int MaxOrganizationUsers { get; set; } = 0; // 0 = unlimited (Professional), 3 = SimpleStart limit
}

// Property & Tenant Lifecycle Enums
Expand Down
17 changes: 17 additions & 0 deletions 0-Aquiis.Core/Entities/MaintenanceRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ public class MaintenanceRequest : BaseModel, ISchedulableEntity
public virtual Property? Property { get; set; }
public virtual Lease? Lease { get; set; }

/// <summary>
/// Collection of repairs that comprise this maintenance request.
/// A MaintenanceRequest CONSISTS of one or more Repairs (composition relationship).
/// Professional product: MR must have at least one repair before completion.
/// </summary>
public virtual ICollection<Repair> Repairs { get; set; } = new List<Repair>();

// Computed properties for repair aggregation
[NotMapped]
public decimal TotalRepairCost => Repairs?.Sum(r => r.Cost) ?? 0;

[NotMapped]
public int RepairCount => Repairs?.Count ?? 0;

[NotMapped]
public int TotalRepairDuration => Repairs?.Sum(r => r.DurationMinutes) ?? 0;

// Computed property for days open
[NotMapped]
public int DaysOpen
Expand Down
2 changes: 1 addition & 1 deletion 0-Aquiis.Core/Entities/Organization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class Organization
public bool IsDeleted { get; set; } = false;

// Navigation properties
public virtual ICollection<UserOrganization> UserOrganizations { get; set; } = new List<UserOrganization>();
public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; } = new List<OrganizationUser>();
public virtual ICollection<Property> Properties { get; set; } = new List<Property>();
public virtual ICollection<Tenant> Tenants { get; set; } = new List<Tenant>();
public virtual ICollection<Lease> Leases { get; set; } = new List<Lease>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ namespace Aquiis.Core.Entities
/// <summary>
/// Junction table for multi-organization user assignments with role-based permissions
/// </summary>
public class UserOrganization
public class OrganizationUser
{

[RequiredGuid]
[Display(Name = "UserOrganization ID")]
[Display(Name = "OrganizationUser ID")]
public Guid Id { get; set; } = Guid.NewGuid();

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions 0-Aquiis.Core/Entities/Payment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public class Payment : BaseModel
[Display(Name = "Organization ID")]
public Guid OrganizationId { get; set; } = Guid.Empty;

[Required]
[StringLength(50)]
[Display(Name = "Payment Number")]
public string PaymentNumber { get; set; } = string.Empty;

[Required]
public Guid InvoiceId { get; set; }

Expand Down
2 changes: 2 additions & 0 deletions 0-Aquiis.Core/Entities/Property.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ public class Property : BaseModel
// Navigation properties
public virtual ICollection<Lease> Leases { get; set; } = new List<Lease>();
public virtual ICollection<Document> Documents { get; set; } = new List<Document>();
public virtual ICollection<Repair> Repairs { get; set; } = new List<Repair>();
public virtual ICollection<MaintenanceRequest> MaintenanceRequests { get; set; } = new List<MaintenanceRequest>();

// Computed property for pending application count
[NotMapped]
Expand Down
134 changes: 134 additions & 0 deletions 0-Aquiis.Core/Entities/Repair.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Aquiis.Core.Validation;

namespace Aquiis.Core.Entities;

/// <summary>
/// Represents work performed on a property WITHOUT workflow/status tracking.
/// Repairs capture what work was done (or is being done) without the complexity
/// of MaintenanceRequest workflow (no Status, Priority, or Assignment).
/// Use CompletedOn to determine if work is finished (null = in progress, date = completed).
/// </summary>
public class Repair : BaseModel
{
// Core Identity
[RequiredGuid]
[Display(Name = "Organization ID")]
public Guid OrganizationId { get; set; } = Guid.Empty;

[RequiredGuid]
public Guid PropertyId { get; set; }

// Optional Relationships (Soft References)
/// <summary>
/// Optional: Links this repair to a MaintenanceRequest workflow (Professional product).
/// Null for standalone repairs (SimpleStart product).
/// </summary>
public Guid? MaintenanceRequestId { get; set; }

/// <summary>
/// Optional: Links this repair to a specific lease/tenant.
/// </summary>
public Guid? LeaseId { get; set; }

// Repair Details
[Required]
[StringLength(200)]
[Display(Name = "Description")]
public string Description { get; set; } = string.Empty;

[StringLength(50)]
[Display(Name = "Repair Type")]
public string RepairType { get; set; } = string.Empty; // From ApplicationConstants.RepairTypes

/// <summary>
/// Optional: Date when work was actually completed.
/// Null = work in progress or not yet completed.
/// Date = work finished on this date.
/// Since Repairs have no Status field, CompletedOn provides definitive completion date.
/// </summary>
[Display(Name = "Completed On")]
public DateTime? CompletedOn { get; set; }

// Cost & Duration
[Column(TypeName = "decimal(18,2)")]
[Display(Name = "Cost")]
public decimal Cost { get; set; }

[Display(Name = "Duration (Minutes)")]
public int DurationMinutes { get; set; } // Time spent on repair

// Who did the work
[StringLength(100)]
[Display(Name = "Contractor/Company Name")]
public string ContractorName { get; set; } = string.Empty; // Company or person name (e.g., "ABC Plumbing & Heating" or "John Doe")

[StringLength(100)]
[Display(Name = "Contact Person")]
public string ContactPerson { get; set; } = string.Empty; // Specific person at company (optional for companies)

[StringLength(20)]
[Display(Name = "Contractor Phone")]
public string ContractorPhone { get; set; } = string.Empty; // Optional: "555-1234"

/// <summary>
/// Optional: Link to Contractor entity when contractor list is implemented.
/// When null, contractor details are stored in text fields (ContractorName/ContactPerson/ContractorPhone).
/// </summary>
[Display(Name = "Contractor ID")]
public Guid? ContractorId { get; set; }

/// <summary>
/// Future: Link to formal Contact entity when added to the system.
/// </summary>
public Guid? ContactId { get; set; }

// Additional Details
[StringLength(2000)]
[Display(Name = "Notes")]
public string Notes { get; set; } = string.Empty;

[StringLength(500)]
[Display(Name = "Parts Replaced")]
public string PartsReplaced { get; set; } = string.Empty; // e.g., "Faucet cartridge, shower seal"

[Display(Name = "Warranty Applies")]
public bool WarrantyApplies { get; set; } = false;

[Display(Name = "Warranty Expires On")]
public DateTime? WarrantyExpiresOn { get; set; }

// Navigation Properties
public virtual Property? Property { get; set; }
public virtual MaintenanceRequest? MaintenanceRequest { get; set; }
public virtual Lease? Lease { get; set; }

// Computed Properties
[NotMapped]
[Display(Name = "Duration")]
public string DurationDisplay
{
get
{
if (DurationMinutes < 60)
return $"{DurationMinutes} minutes";

var hours = DurationMinutes / 60;
var minutes = DurationMinutes % 60;
return minutes > 0
? $"{hours}h {minutes}m"
: $"{hours} hour{(hours > 1 ? "s" : "")}";
}
}

[NotMapped]
[Display(Name = "Under Warranty")]
public bool IsUnderWarranty => WarrantyApplies &&
WarrantyExpiresOn.HasValue &&
WarrantyExpiresOn.Value > DateTime.Today;

[NotMapped]
[Display(Name = "Completed")]
public bool IsCompleted => CompletedOn.HasValue; //Provides quick check for completion status. If no value, work is In Progess in the display.
}
54 changes: 54 additions & 0 deletions 0-Aquiis.Core/Entities/UserProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;

namespace Aquiis.Core.Entities;

/// <summary>
/// User profile information stored in business context (ApplicationDbContext).
/// Separate from Identity (AspNetUsers) to enable single-context queries with business entities.
/// Email is denormalized for query simplification (considered read-only in B2B context).
/// </summary>
public class UserProfile : BaseModel
{
/// <summary>
/// Foreign key to AspNetUsers.Id (Identity context).
/// This links the profile to the authentication user.
/// </summary>
public string UserId { get; set; } = string.Empty;

// Personal Information (denormalized from Identity for query efficiency)

/// <summary>
/// User's email address (cached from AspNetUsers).
/// Considered read-only - changes require sync with Identity table.
/// </summary>
public string Email { get; set; } = string.Empty;

public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string? PhoneNumber { get; set; }

// Organization Context

/// <summary>
/// User's "home" organization - their primary/default organization.
/// </summary>
public Guid? OrganizationId { get; set; }

/// <summary>
/// Currently active organization the user is viewing/working with.
/// Can differ from home organization for users with multi-org access.
/// </summary>
public Guid? ActiveOrganizationId { get; set; }

// Computed Properties

/// <summary>
/// Full name combining first and last name.
/// </summary>
public string FullName => $"{FirstName} {LastName}".Trim();

/// <summary>
/// Display name for UI - uses full name if available, falls back to email.
/// </summary>
public string DisplayName => string.IsNullOrWhiteSpace(FullName) ? Email : FullName;
}
Loading