Skip to content

Commit 033a70c

Browse files
author
CIS Guru
committed
Phase 2.4 notification infrastructure complete
1 parent 1d95756 commit 033a70c

13 files changed

Lines changed: 5573 additions & 36 deletions
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
2+
using Aquiis.SimpleStart.Core.Constants;
3+
using Aquiis.SimpleStart.Core.Interfaces.Services;
4+
using Aquiis.SimpleStart.Core.Services;
5+
using Aquiis.SimpleStart.Infrastructure.Data;
6+
using Aquiis.SimpleStart.Shared.Services;
7+
using Microsoft.EntityFrameworkCore;
8+
using Microsoft.Extensions.Options;
9+
10+
namespace Aquiis.SimpleStart.Application.Services;
11+
public class NotificationService : BaseService<Notification>
12+
{
13+
private readonly IEmailService _emailService;
14+
private readonly ISMSService _smsService;
15+
private new readonly ILogger<NotificationService> _logger;
16+
17+
public NotificationService(
18+
ApplicationDbContext context,
19+
UserContextService userContext,
20+
IEmailService emailService,
21+
ISMSService smsService,
22+
IOptions<ApplicationSettings> appSettings,
23+
ILogger<NotificationService> logger)
24+
: base(context, logger, userContext, appSettings)
25+
{
26+
_emailService = emailService;
27+
_smsService = smsService;
28+
_logger = logger;
29+
}
30+
31+
/// <summary>
32+
/// Create and send a notification to a user
33+
/// </summary>
34+
public async Task<Notification> SendNotificationAsync(
35+
string recipientUserId,
36+
string title,
37+
string message,
38+
string type,
39+
string category,
40+
Guid? relatedEntityId = null,
41+
string? relatedEntityType = null)
42+
{
43+
var organizationId = await _userContext.GetActiveOrganizationIdAsync();
44+
45+
// Get user preferences
46+
var preferences = await GetNotificationPreferencesAsync(recipientUserId);
47+
48+
var notification = new Notification
49+
{
50+
Id = Guid.NewGuid(),
51+
OrganizationId = organizationId!.Value,
52+
RecipientUserId = recipientUserId,
53+
Title = title,
54+
Message = message,
55+
Type = type,
56+
Category = category,
57+
RelatedEntityId = relatedEntityId,
58+
RelatedEntityType = relatedEntityType,
59+
SentOn = DateTime.UtcNow,
60+
IsRead = false,
61+
SendInApp = preferences.EnableInAppNotifications,
62+
SendEmail = preferences.EnableEmailNotifications && ShouldSendEmail(category, preferences),
63+
SendSMS = preferences.EnableSMSNotifications && ShouldSendSMS(category, preferences)
64+
};
65+
66+
// Save in-app notification
67+
await CreateAsync(notification);
68+
69+
// Send email if enabled
70+
if (notification.SendEmail && !string.IsNullOrEmpty(preferences.EmailAddress))
71+
{
72+
try
73+
{
74+
await _emailService.SendEmailAsync(
75+
preferences.EmailAddress,
76+
title,
77+
message);
78+
79+
notification.EmailSent = true;
80+
notification.EmailSentOn = DateTime.UtcNow;
81+
}
82+
catch (Exception ex)
83+
{
84+
_logger.LogError(ex, $"Failed to send email notification to {recipientUserId}");
85+
notification.EmailError = ex.Message;
86+
}
87+
}
88+
89+
// Send SMS if enabled
90+
if (notification.SendSMS && !string.IsNullOrEmpty(preferences.PhoneNumber))
91+
{
92+
try
93+
{
94+
await _smsService.SendSMSAsync(
95+
preferences.PhoneNumber,
96+
$"{title}: {message}");
97+
98+
notification.SMSSent = true;
99+
notification.SMSSentOn = DateTime.UtcNow;
100+
}
101+
catch (Exception ex)
102+
{
103+
_logger.LogError(ex, $"Failed to send SMS notification to {recipientUserId}");
104+
notification.SMSError = ex.Message;
105+
}
106+
}
107+
108+
await UpdateAsync(notification);
109+
110+
return notification;
111+
}
112+
113+
/// <summary>
114+
/// Mark notification as read
115+
/// </summary>
116+
public async Task MarkAsReadAsync(Guid notificationId)
117+
{
118+
var notification = await GetByIdAsync(notificationId);
119+
if (notification == null) return;
120+
121+
notification.IsRead = true;
122+
notification.ReadOn = DateTime.UtcNow;
123+
124+
await UpdateAsync(notification);
125+
}
126+
127+
/// <summary>
128+
/// Get unread notifications for current user
129+
/// </summary>
130+
public async Task<List<Notification>> GetUnreadNotificationsAsync()
131+
{
132+
var userId = await _userContext.GetUserIdAsync();
133+
var organizationId = await _userContext.GetActiveOrganizationIdAsync();
134+
135+
return await _context.Notifications
136+
.Where(n => n.OrganizationId == organizationId
137+
&& n.RecipientUserId == userId
138+
&& !n.IsRead
139+
&& !n.IsDeleted)
140+
.OrderByDescending(n => n.SentOn)
141+
.Take(50)
142+
.ToListAsync();
143+
}
144+
145+
/// <summary>
146+
/// Get notification history for current user
147+
/// </summary>
148+
public async Task<List<Notification>> GetNotificationHistoryAsync(int count = 100)
149+
{
150+
var userId = await _userContext.GetUserIdAsync();
151+
var organizationId = await _userContext.GetActiveOrganizationIdAsync();
152+
153+
return await _context.Notifications
154+
.Where(n => n.OrganizationId == organizationId
155+
&& n.RecipientUserId == userId
156+
&& !n.IsDeleted)
157+
.OrderByDescending(n => n.SentOn)
158+
.Take(count)
159+
.ToListAsync();
160+
}
161+
162+
/// <summary>
163+
/// Get or create notification preferences for user
164+
/// </summary>
165+
private async Task<NotificationPreferences> GetNotificationPreferencesAsync(string userId)
166+
{
167+
var organizationId = await _userContext.GetActiveOrganizationIdAsync();
168+
169+
var preferences = await _context.NotificationPreferences
170+
.FirstOrDefaultAsync(p => p.OrganizationId == organizationId
171+
&& p.UserId == userId
172+
&& !p.IsDeleted);
173+
174+
if (preferences == null)
175+
{
176+
// Create default preferences
177+
preferences = new NotificationPreferences
178+
{
179+
Id = Guid.NewGuid(),
180+
OrganizationId = organizationId!.Value,
181+
UserId = userId,
182+
EnableInAppNotifications = true,
183+
EnableEmailNotifications = true,
184+
EnableSMSNotifications = false,
185+
EmailLeaseExpiring = true,
186+
EmailPaymentDue = true,
187+
EmailPaymentReceived = true,
188+
EmailApplicationStatusChange = true,
189+
EmailMaintenanceUpdate = true,
190+
EmailInspectionScheduled = true
191+
};
192+
193+
_context.NotificationPreferences.Add(preferences);
194+
await _context.SaveChangesAsync();
195+
}
196+
197+
return preferences;
198+
}
199+
200+
private bool ShouldSendEmail(string category, NotificationPreferences prefs)
201+
{
202+
return category switch
203+
{
204+
NotificationConstants.Categories.Lease => prefs.EmailLeaseExpiring,
205+
NotificationConstants.Categories.Payment => prefs.EmailPaymentDue,
206+
NotificationConstants.Categories.Application => prefs.EmailApplicationStatusChange,
207+
NotificationConstants.Categories.Maintenance => prefs.EmailMaintenanceUpdate,
208+
NotificationConstants.Categories.Inspection => prefs.EmailInspectionScheduled,
209+
_ => true
210+
};
211+
}
212+
213+
private bool ShouldSendSMS(string category, NotificationPreferences prefs)
214+
{
215+
return category switch
216+
{
217+
NotificationConstants.Categories.Payment => prefs.SMSPaymentDue,
218+
NotificationConstants.Categories.Maintenance => prefs.SMSMaintenanceEmergency,
219+
NotificationConstants.Categories.Lease => prefs.SMSLeaseExpiringUrgent,
220+
_ => false
221+
};
222+
}
223+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
public static class NotificationConstants
2+
{
3+
public static class Types
4+
{
5+
public const string Info = "Info";
6+
public const string Warning = "Warning";
7+
public const string Error = "Error";
8+
public const string Success = "Success";
9+
}
10+
11+
public static class Categories
12+
{
13+
public const string Lease = "Lease";
14+
public const string Payment = "Payment";
15+
public const string Maintenance = "Maintenance";
16+
public const string Application = "Application";
17+
public const string Property = "Property";
18+
public const string Inspection = "Inspection";
19+
public const string Document = "Document";
20+
public const string System = "System";
21+
}
22+
23+
public static class Templates
24+
{
25+
// Lease notifications
26+
public const string LeaseExpiring90Days = "lease_expiring_90";
27+
public const string LeaseExpiring60Days = "lease_expiring_60";
28+
public const string LeaseExpiring30Days = "lease_expiring_30";
29+
public const string LeaseActivated = "lease_activated";
30+
public const string LeaseTerminated = "lease_terminated";
31+
32+
// Payment notifications
33+
public const string PaymentDueReminder = "payment_due_reminder";
34+
public const string PaymentReceived = "payment_received";
35+
public const string PaymentLate = "payment_late";
36+
public const string LateFeeApplied = "late_fee_applied";
37+
38+
// Maintenance notifications
39+
public const string MaintenanceRequestCreated = "maintenance_created";
40+
public const string MaintenanceRequestAssigned = "maintenance_assigned";
41+
public const string MaintenanceRequestStarted = "maintenance_started";
42+
public const string MaintenanceRequestCompleted = "maintenance_completed";
43+
44+
// Application notifications
45+
public const string ApplicationSubmitted = "application_submitted";
46+
public const string ApplicationUnderReview = "application_under_review";
47+
public const string ApplicationApproved = "application_approved";
48+
public const string ApplicationRejected = "application_rejected";
49+
50+
// Inspection notifications
51+
public const string InspectionScheduled = "inspection_scheduled";
52+
public const string InspectionCompleted = "inspection_completed";
53+
54+
// Document notifications
55+
public const string DocumentUploaded = "document_uploaded";
56+
public const string DocumentExpiring = "document_expiring";
57+
}
58+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.ComponentModel.DataAnnotations.Schema;
3+
using Aquiis.SimpleStart.Core.Entities;
4+
using Aquiis.SimpleStart.Core.Validation;
5+
6+
public class Notification : BaseModel
7+
{
8+
[RequiredGuid]
9+
public Guid OrganizationId { get; set; }
10+
11+
[Required]
12+
[StringLength(200)]
13+
public string Title { get; set; } = string.Empty;
14+
15+
[Required]
16+
[StringLength(2000)]
17+
public string Message { get; set; } = string.Empty;
18+
19+
[Required]
20+
[StringLength(50)]
21+
public string Type { get; set; } = string.Empty; // Info, Warning, Error, Success
22+
23+
[Required]
24+
[StringLength(50)]
25+
public string Category { get; set; } = string.Empty; // Lease, Payment, Maintenance, Application
26+
27+
[Required]
28+
public string RecipientUserId { get; set; } = string.Empty;
29+
30+
[Required]
31+
public DateTime SentOn { get; set; }
32+
33+
public DateTime? ReadOn { get; set; }
34+
35+
public bool IsRead { get; set; }
36+
37+
// Optional entity reference for "view details" link
38+
public Guid? RelatedEntityId { get; set; }
39+
40+
[StringLength(50)]
41+
public string? RelatedEntityType { get; set; }
42+
43+
// Delivery channels
44+
public bool SendInApp { get; set; } = true;
45+
public bool SendEmail { get; set; }
46+
public bool SendSMS { get; set; }
47+
48+
// Delivery status
49+
public bool EmailSent { get; set; }
50+
public DateTime? EmailSentOn { get; set; }
51+
52+
public bool SMSSent { get; set; }
53+
public DateTime? SMSSentOn { get; set; }
54+
55+
[StringLength(500)]
56+
public string? EmailError { get; set; }
57+
58+
[StringLength(500)]
59+
public string? SMSError { get; set; }
60+
61+
// Navigation
62+
[ForeignKey(nameof(OrganizationId))]
63+
public virtual Organization? Organization { get; set; }
64+
}

0 commit comments

Comments
 (0)