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
4 changes: 4 additions & 0 deletions 2-Aquiis.Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,19 @@ public static IServiceCollection AddApplication(
services.AddScoped<CalendarEventService>();
services.AddScoped<CalendarSettingsService>();
services.AddScoped<ChecklistService>();
services.AddScoped<DigestService>();
services.AddScoped<DocumentNotificationService>();
services.AddScoped<DocumentService>();
services.AddScoped<EmailService>();
services.AddScoped<EmailSettingsService>();
services.AddScoped<FinancialReportService>();
services.AddScoped<InspectionService>();
services.AddScoped<InvoiceService>();
services.AddScoped<LeaseNotificationService>();
services.AddScoped<LeaseOfferService>();
services.AddScoped<LeaseService>();
services.AddScoped<LeaseWorkflowService>();
services.AddScoped<MaintenanceNotificationService>();
services.AddScoped<MaintenanceService>();
services.AddScoped<NoteService>();
services.AddScoped<NotificationService>();
Expand Down
653 changes: 653 additions & 0 deletions 2-Aquiis.Application/Services/DigestService.cs

Large diffs are not rendered by default.

183 changes: 183 additions & 0 deletions 2-Aquiis.Application/Services/DocumentNotificationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using Aquiis.Core.Constants;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Aquiis.Application.Services;

/// <summary>
/// Service responsible for checking document expirations and sending reminders.
/// Monitors lease documents, insurance certificates, and property-related documents for expiration.
/// </summary>
public class DocumentNotificationService
{
private readonly ILogger<DocumentNotificationService> _logger;
private readonly ApplicationDbContext _dbContext;
private readonly NotificationService _notificationService;

public DocumentNotificationService(
ILogger<DocumentNotificationService> logger,
ApplicationDbContext dbContext,
NotificationService notificationService)
{
_logger = logger;
_dbContext = dbContext;
_notificationService = notificationService;
}

/// <summary>
/// Checks for old documents (lease agreements, insurance certs) that may need renewal.
/// Sends notifications to admin users for documents older than 180 days.
/// </summary>
public async Task CheckDocumentExpirationsAsync()
{
try
{
_logger.LogInformation("Starting document expiration check at {Time}", DateTime.Now);

var now = DateTime.Now;
var days180 = now.AddDays(-180);

var organizations = await _dbContext.Organizations
.Where(o => !o.IsDeleted)
.Select(o => o.Id)
.ToListAsync();

int notificationsCreated = 0;

foreach (var organizationId in organizations)
{
// Get admin users for this organization
var adminUsers = await _dbContext.UserOrganizations
.Where(uom => uom.OrganizationId == organizationId &&
uom.Role == "Admin" &&
!uom.IsDeleted)
.ToListAsync();

if (!adminUsers.Any())
{
_logger.LogWarning("No admin users found for organization {OrgId}", organizationId);
continue;
}

// Check for old lease documents (6+ months old)
var oldLeaseDocuments = await _dbContext.Documents
.Where(d => d.OrganizationId == organizationId &&
!d.IsDeleted &&
d.LeaseId != null &&
d.CreatedOn < days180 &&
d.DocumentType == ApplicationConstants.DocumentTypes.LeaseAgreement)
.Include(d => d.Lease)
.ThenInclude(l => l!.Property)
.Take(5) // Limit to 5 per check to avoid spam
.ToListAsync();

foreach (var doc in oldLeaseDocuments)
{
if (doc.Lease?.Property != null)
{
var daysOld = (now - doc.CreatedOn).Days;
var title = "Document Review Needed";
var message = $"Lease document for {doc.Lease.Property.Address} was created {daysOld} days ago. Please review if renewal or update is needed.";

foreach (var user in adminUsers)
{
await _notificationService.SendNotificationAsync(
recipientUserId: user.UserId,
title: title,
message: message,
type: "System",
category: "DocumentReview",
relatedEntityId: doc.Id,
relatedEntityType: "Document"
);
notificationsCreated++;
}
}
}

// Check for old property insurance documents (6+ months old)
// Note: Since Insurance/Certificate are not in DocumentTypes, checking description/filename
var oldPropertyInsurance = await _dbContext.Documents
.Where(d => d.OrganizationId == organizationId &&
!d.IsDeleted &&
d.PropertyId != null &&
d.CreatedOn < days180 &&
(d.Description.ToLower().Contains("insurance") ||
d.Description.ToLower().Contains("certificate") ||
d.FileName.ToLower().Contains("insurance") ||
d.FileName.ToLower().Contains("certificate")))
.Include(d => d.Property)
.Take(5)
.ToListAsync();

foreach (var doc in oldPropertyInsurance)
{
if (doc.Property != null)
{
var daysOld = (now - doc.CreatedOn).Days;
var title = "Document Review Needed";
var message = $"Insurance/Certificate document for {doc.Property.Address} is {daysOld} days old. Please verify current coverage.";

foreach (var user in adminUsers)
{
await _notificationService.SendNotificationAsync(
recipientUserId: user.UserId,
title: title,
message: message,
type: "System",
category: "DocumentReview",
relatedEntityId: doc.Id,
relatedEntityType: "Document"
);
notificationsCreated++;
}
}
}

// Check for old tenant documents - 1 year old
var days365 = now.AddDays(-365);
var oldTenantDocuments = await _dbContext.Documents
.Where(d => d.OrganizationId == organizationId &&
!d.IsDeleted &&
d.TenantId != null &&
d.CreatedOn < days365)
.Include(d => d.Tenant)
.Take(5)
.ToListAsync();

foreach (var doc in oldTenantDocuments)
{
if (doc.Tenant != null)
{
var daysOld = (now - doc.CreatedOn).Days;
var title = "Document Review Needed";
var message = $"{doc.DocumentType} for tenant {doc.Tenant.FullName} is {daysOld} days old. Please verify if update is needed.";

foreach (var user in adminUsers)
{
await _notificationService.SendNotificationAsync(
recipientUserId: user.UserId,
title: title,
message: message,
type: "System",
category: "DocumentReview",
relatedEntityId: doc.Id,
relatedEntityType: "Document"
);
notificationsCreated++;
}
}
}
}

_logger.LogInformation(
"Document expiration check complete: {NotificationCount} notifications created across {OrgCount} organizations",
notificationsCreated,
organizations.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking document expirations");
}
}
}
189 changes: 189 additions & 0 deletions 2-Aquiis.Application/Services/LeaseNotificationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
using Aquiis.Core.Constants;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Aquiis.Application.Services;

/// <summary>
/// Service responsible for monitoring lease expirations and sending renewal reminders.
/// Sends notifications at 90, 60, and 30 days before lease expiration.
/// </summary>
public class LeaseNotificationService
{
private readonly ILogger<LeaseNotificationService> _logger;
private readonly ApplicationDbContext _dbContext;
private readonly NotificationService _notificationService;

public LeaseNotificationService(
ILogger<LeaseNotificationService> logger,
ApplicationDbContext dbContext,
NotificationService notificationService)
{
_logger = logger;
_dbContext = dbContext;
_notificationService = notificationService;
}

/// <summary>
/// Checks for leases expiring and sends renewal notifications at 90, 60, and 30 day marks.
/// Updates lease renewal tracking fields accordingly.
/// </summary>
public async Task SendLeaseRenewalRemindersAsync(Guid organizationId, CancellationToken stoppingToken = default)
{
try
{
var today = DateTime.Today;
var addresses = string.Empty;

// Check for leases expiring in 90 days (initial notification)
var leasesExpiring90Days = await _dbContext.Leases
.Include(l => l.Tenant)
.Include(l => l.Property)
.Where(l => !l.IsDeleted &&
l.OrganizationId == organizationId &&
l.Status == "Active" &&
l.EndDate >= today.AddDays(85) &&
l.EndDate <= today.AddDays(95) &&
(l.RenewalNotificationSent == null || !l.RenewalNotificationSent.Value))
.ToListAsync(stoppingToken);

foreach (var lease in leasesExpiring90Days)
{
_logger.LogInformation(
"Lease expiring in 90 days: Lease ID {LeaseId}, Property: {PropertyAddress}, Tenant: {TenantName}, End Date: {EndDate}",
lease.Id,
lease.Property?.Address ?? "Unknown",
lease.Tenant?.FullName ?? "Unknown",
lease.EndDate.ToString("MMM dd, yyyy"));

lease.RenewalNotificationSent = true;
lease.RenewalNotificationSentOn = DateTime.UtcNow;
lease.RenewalStatus = "Pending";
lease.LastModifiedOn = DateTime.UtcNow;
lease.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task

addresses += lease.Property?.Address + "\n";
}

// Send organization-wide notification if any leases are expiring
if (!string.IsNullOrEmpty(addresses))
{
await _notificationService.NotifyAllUsersAsync(
organizationId,
"90-Day Lease Renewal Notification",
$"The following properties have leases expiring in 90 days:\n\n{addresses}",
NotificationConstants.Types.Info,
NotificationConstants.Categories.Lease,
null,
ApplicationConstants.EntityTypes.Lease);
}

// clear addresses for next use
addresses = string.Empty;

// Check for leases expiring in 60 days (reminder)
var leasesExpiring60Days = await _dbContext.Leases
.Include(l => l.Tenant)
.Include(l => l.Property)
.Where(l => !l.IsDeleted &&
l.OrganizationId == organizationId &&
l.Status == "Active" &&
l.EndDate >= today.AddDays(55) &&
l.EndDate <= today.AddDays(65) &&
l.RenewalNotificationSent == true &&
l.RenewalReminderSentOn == null)
.ToListAsync(stoppingToken);

foreach (var lease in leasesExpiring60Days)
{
_logger.LogInformation(
"Lease expiring in 60 days (reminder): Lease ID {LeaseId}, Property: {PropertyAddress}, Tenant: {TenantName}, End Date: {EndDate}",
lease.Id,
lease.Property?.Address ?? "Unknown",
lease.Tenant?.FullName ?? "Unknown",
lease.EndDate.ToString("MMM dd, yyyy"));

lease.RenewalReminderSentOn = DateTime.UtcNow;
lease.LastModifiedOn = DateTime.UtcNow;
lease.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task

addresses += lease.Property?.Address + "\n";
}

// Send organization-wide notification if any leases are expiring
if (!string.IsNullOrEmpty(addresses))
{
await _notificationService.NotifyAllUsersAsync(
organizationId,
"60-Day Lease Renewal Notification",
$"The following properties have leases expiring in 60 days:\n\n{addresses}",
NotificationConstants.Types.Info,
NotificationConstants.Categories.Lease,
null,
ApplicationConstants.EntityTypes.Lease);
}

// clear addresses for next use
addresses = string.Empty;

// Check for leases expiring in 30 days (final reminder)
var leasesExpiring30Days = await _dbContext.Leases
.Include(l => l.Tenant)
.Include(l => l.Property)
.Where(l => !l.IsDeleted &&
l.OrganizationId == organizationId &&
l.Status == "Active" &&
l.EndDate >= today.AddDays(25) &&
l.EndDate <= today.AddDays(35) &&
l.RenewalStatus == "Pending")
.ToListAsync(stoppingToken);

foreach (var lease in leasesExpiring30Days)
{
_logger.LogInformation(
"Lease expiring in 30 days (final reminder): Lease ID {LeaseId}, Property: {PropertyAddress}, Tenant: {TenantName}, End Date: {EndDate}",
lease.Id,
lease.Property?.Address ?? "Unknown",
lease.Tenant?.FullName ?? "Unknown",
lease.EndDate.ToString("MMM dd, yyyy"));

addresses += lease.Property?.Address + "\n";
}

// Send organization-wide notification if any leases are expiring
if (!string.IsNullOrEmpty(addresses))
{
await _notificationService.NotifyAllUsersAsync(
organizationId,
"30-Day Lease Renewal Notification",
$"The following properties have leases expiring in 30 days:\n\n{addresses}",
NotificationConstants.Types.Info,
NotificationConstants.Categories.Lease,
null,
ApplicationConstants.EntityTypes.Lease);
}

// Save all updates
var totalUpdated = leasesExpiring90Days.Count + leasesExpiring60Days.Count +
leasesExpiring30Days.Count;

if (totalUpdated > 0)
{
await _dbContext.SaveChangesAsync(stoppingToken);
_logger.LogInformation(
"Processed {Count} lease renewal notifications for organization {OrganizationId}: {Initial} initial, {Reminder60} 60-day, {Reminder30} 30-day reminders",
totalUpdated,
organizationId,
leasesExpiring90Days.Count,
leasesExpiring60Days.Count,
leasesExpiring30Days.Count);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error checking lease renewals for organization {OrganizationId}",
organizationId);
}
}
}
Loading
Loading