Skip to content

Commit 3ea5fef

Browse files
authored
Merge pull request #13 from xnodeoncode/development
Phase 2.5 Email & SMS Integration
2 parents 75f83b1 + f6700cc commit 3ea5fef

164 files changed

Lines changed: 76491 additions & 12 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Aquiis.SimpleStart.Core.Constants;
4+
using Aquiis.SimpleStart.Core.Entities;
5+
using Aquiis.SimpleStart.Core.Interfaces.Services;
6+
using Aquiis.SimpleStart.Core.Services;
7+
using Aquiis.SimpleStart.Infrastructure.Data;
8+
using Aquiis.SimpleStart.Infrastructure.Services;
9+
using Aquiis.SimpleStart.Shared.Services;
10+
using Microsoft.EntityFrameworkCore;
11+
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Options;
13+
using SendGrid;
14+
using SendGrid.Helpers.Mail;
15+
16+
namespace Aquiis.SimpleStart.Application.Services
17+
{
18+
public class EmailSettingsService : BaseService<OrganizationEmailSettings>
19+
{
20+
private readonly SendGridEmailService _emailService;
21+
22+
public EmailSettingsService(
23+
ApplicationDbContext context,
24+
ILogger<EmailSettingsService> logger,
25+
UserContextService userContext,
26+
IOptions<ApplicationSettings> settings,
27+
SendGridEmailService emailService)
28+
: base(context, logger, userContext, settings)
29+
{
30+
_emailService = emailService;
31+
}
32+
33+
/// <summary>
34+
/// Get email settings for current organization or create default disabled settings
35+
/// </summary>
36+
public async Task<OrganizationEmailSettings> GetOrCreateSettingsAsync()
37+
{
38+
var orgId = await _userContext.GetActiveOrganizationIdAsync();
39+
if (orgId == null)
40+
{
41+
throw new UnauthorizedAccessException("No active organization");
42+
}
43+
44+
var settings = await _dbSet
45+
.FirstOrDefaultAsync(s => s.OrganizationId == orgId && !s.IsDeleted);
46+
47+
if (settings == null)
48+
{
49+
settings = new OrganizationEmailSettings
50+
{
51+
Id = Guid.NewGuid(),
52+
OrganizationId = orgId.Value,
53+
IsEmailEnabled = false,
54+
DailyLimit = 100, // SendGrid free tier default
55+
MonthlyLimit = 40000,
56+
CreatedBy = await _userContext.GetUserIdAsync() ?? string.Empty,
57+
CreatedOn = DateTime.UtcNow
58+
};
59+
await CreateAsync(settings);
60+
}
61+
62+
return settings;
63+
}
64+
65+
/// <summary>
66+
/// Configure SendGrid API key and enable email functionality
67+
/// </summary>
68+
public async Task<OperationResult> UpdateSendGridConfigAsync(
69+
string apiKey,
70+
string fromEmail,
71+
string fromName)
72+
{
73+
// Verify the API key works before saving
74+
if (!await _emailService.VerifyApiKeyAsync(apiKey))
75+
{
76+
return OperationResult.FailureResult(
77+
"Invalid SendGrid API key. Please verify the key has Mail Send permissions.");
78+
}
79+
80+
var settings = await GetOrCreateSettingsAsync();
81+
82+
settings.SendGridApiKeyEncrypted = _emailService.EncryptApiKey(apiKey);
83+
settings.FromEmail = fromEmail;
84+
settings.FromName = fromName;
85+
settings.IsEmailEnabled = true;
86+
settings.IsVerified = true;
87+
settings.LastVerifiedOn = DateTime.UtcNow;
88+
settings.LastError = null;
89+
90+
await UpdateAsync(settings);
91+
92+
return OperationResult.SuccessResult("SendGrid configuration saved successfully");
93+
}
94+
95+
/// <summary>
96+
/// Disable email functionality for organization
97+
/// </summary>
98+
public async Task<OperationResult> DisableEmailAsync()
99+
{
100+
var settings = await GetOrCreateSettingsAsync();
101+
settings.IsEmailEnabled = false;
102+
await UpdateAsync(settings);
103+
104+
return OperationResult.SuccessResult("Email notifications disabled");
105+
}
106+
107+
/// <summary>
108+
/// Re-enable email functionality
109+
/// </summary>
110+
public async Task<OperationResult> EnableEmailAsync()
111+
{
112+
var settings = await GetOrCreateSettingsAsync();
113+
114+
if (string.IsNullOrEmpty(settings.SendGridApiKeyEncrypted))
115+
{
116+
return OperationResult.FailureResult(
117+
"SendGrid API key not configured. Please configure SendGrid first.");
118+
}
119+
120+
settings.IsEmailEnabled = true;
121+
await UpdateAsync(settings);
122+
123+
return OperationResult.SuccessResult("Email notifications enabled");
124+
}
125+
126+
/// <summary>
127+
/// Send a test email to verify configuration
128+
/// </summary>
129+
public async Task<OperationResult> TestEmailConfigurationAsync(string testEmail)
130+
{
131+
try
132+
{
133+
await _emailService.SendEmailAsync(
134+
testEmail,
135+
"Aquiis Email Configuration Test",
136+
"<h2>Configuration Test Successful!</h2>" +
137+
"<p>This is a test email to verify your SendGrid configuration is working correctly.</p>" +
138+
"<p>If you received this email, your email integration is properly configured.</p>");
139+
140+
return OperationResult.SuccessResult("Test email sent successfully! Check your inbox.");
141+
}
142+
catch (Exception ex)
143+
{
144+
_logger.LogError(ex, "Test email failed");
145+
return OperationResult.FailureResult($"Failed to send test email: {ex.Message}");
146+
}
147+
}
148+
149+
/// <summary>
150+
/// Update email sender information
151+
/// </summary>
152+
public async Task<OperationResult> UpdateSenderInfoAsync(string fromEmail, string fromName)
153+
{
154+
var settings = await GetOrCreateSettingsAsync();
155+
156+
settings.FromEmail = fromEmail;
157+
settings.FromName = fromName;
158+
159+
await UpdateAsync(settings);
160+
161+
return OperationResult.SuccessResult("Sender information updated");
162+
}
163+
}
164+
}

Aquiis.SimpleStart/Application/Services/NotificationService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
using Aquiis.SimpleStart.Core.Constants;
3+
using Aquiis.SimpleStart.Core.Entities;
34
using Aquiis.SimpleStart.Core.Interfaces.Services;
45
using Aquiis.SimpleStart.Core.Services;
56
using Aquiis.SimpleStart.Infrastructure.Data;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Aquiis.SimpleStart.Core.Constants;
4+
using Aquiis.SimpleStart.Core.Entities;
5+
using Aquiis.SimpleStart.Core.Services;
6+
using Aquiis.SimpleStart.Infrastructure.Data;
7+
using Aquiis.SimpleStart.Infrastructure.Services;
8+
using Aquiis.SimpleStart.Shared.Services;
9+
using Microsoft.EntityFrameworkCore;
10+
using Microsoft.Extensions.Logging;
11+
using Microsoft.Extensions.Options;
12+
13+
namespace Aquiis.SimpleStart.Application.Services
14+
{
15+
public class SMSSettingsService : BaseService<OrganizationSMSSettings>
16+
{
17+
private readonly TwilioSMSService _smsService;
18+
19+
public SMSSettingsService(
20+
ApplicationDbContext context,
21+
ILogger<SMSSettingsService> logger,
22+
UserContextService userContext,
23+
IOptions<ApplicationSettings> settings,
24+
TwilioSMSService smsService)
25+
: base(context, logger, userContext, settings)
26+
{
27+
_smsService = smsService;
28+
}
29+
30+
public async Task<OrganizationSMSSettings> GetOrCreateSettingsAsync()
31+
{
32+
var orgId = await _userContext.GetActiveOrganizationIdAsync();
33+
if (orgId == null)
34+
{
35+
throw new UnauthorizedAccessException("No active organization");
36+
}
37+
38+
var settings = await _dbSet
39+
.FirstOrDefaultAsync(s => s.OrganizationId == orgId && !s.IsDeleted);
40+
41+
if (settings == null)
42+
{
43+
settings = new OrganizationSMSSettings
44+
{
45+
Id = Guid.NewGuid(),
46+
OrganizationId = orgId.Value,
47+
IsSMSEnabled = false,
48+
CostPerSMS = 0.0075m, // Approximate US cost
49+
CreatedBy = await _userContext.GetUserIdAsync() ?? string.Empty,
50+
CreatedOn = DateTime.UtcNow
51+
};
52+
await CreateAsync(settings);
53+
}
54+
55+
return settings;
56+
}
57+
58+
public async Task<OperationResult> UpdateTwilioConfigAsync(
59+
string accountSid,
60+
string authToken,
61+
string phoneNumber)
62+
{
63+
// Verify credentials work before saving
64+
if (!await _smsService.VerifyTwilioCredentialsAsync(accountSid, authToken, phoneNumber))
65+
{
66+
return OperationResult.FailureResult(
67+
"Invalid Twilio credentials or phone number. Please verify your Account SID, Auth Token, and phone number.");
68+
}
69+
70+
var settings = await GetOrCreateSettingsAsync();
71+
72+
settings.TwilioAccountSidEncrypted = _smsService.EncryptAccountSid(accountSid);
73+
settings.TwilioAuthTokenEncrypted = _smsService.EncryptAuthToken(authToken);
74+
settings.TwilioPhoneNumber = phoneNumber;
75+
settings.IsSMSEnabled = true;
76+
settings.IsVerified = true;
77+
settings.LastVerifiedOn = DateTime.UtcNow;
78+
settings.LastError = null;
79+
80+
await UpdateAsync(settings);
81+
82+
return OperationResult.SuccessResult("Twilio configuration saved successfully");
83+
}
84+
85+
public async Task<OperationResult> DisableSMSAsync()
86+
{
87+
var settings = await GetOrCreateSettingsAsync();
88+
settings.IsSMSEnabled = false;
89+
await UpdateAsync(settings);
90+
91+
return OperationResult.SuccessResult("SMS notifications disabled");
92+
}
93+
94+
public async Task<OperationResult> EnableSMSAsync()
95+
{
96+
var settings = await GetOrCreateSettingsAsync();
97+
98+
if (string.IsNullOrEmpty(settings.TwilioAccountSidEncrypted))
99+
{
100+
return OperationResult.FailureResult(
101+
"Twilio credentials not configured. Please configure Twilio first.");
102+
}
103+
104+
settings.IsSMSEnabled = true;
105+
await UpdateAsync(settings);
106+
107+
return OperationResult.SuccessResult("SMS notifications enabled");
108+
}
109+
110+
public async Task<OperationResult> TestSMSConfigurationAsync(string testPhoneNumber)
111+
{
112+
try
113+
{
114+
await _smsService.SendSMSAsync(
115+
testPhoneNumber,
116+
"Aquiis SMS Configuration Test: This message confirms your Twilio integration is working correctly.");
117+
118+
return OperationResult.SuccessResult("Test SMS sent successfully! Check your phone.");
119+
}
120+
catch (Exception ex)
121+
{
122+
_logger.LogError(ex, "Test SMS failed");
123+
return OperationResult.FailureResult($"Failed to send test SMS: {ex.Message}");
124+
}
125+
}
126+
}
127+
}

Aquiis.SimpleStart/Aquiis.SimpleStart.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@
4040
<PrivateAssets>all</PrivateAssets>
4141
</PackageReference>
4242
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
43+
<PackageReference Include="SendGrid" Version="9.29.3" />
4344
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
4445
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" />
4546
<PackageReference Include="QuestPDF" Version="2025.7.4" />
47+
<PackageReference Include="Twilio" Version="7.14.0" />
4648
</ItemGroup>
4749

4850
</Project>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace Aquiis.SimpleStart.Core.Entities
2+
{
3+
public class OperationResult
4+
{
5+
public bool Success { get; set; }
6+
public string Message { get; set; } = string.Empty;
7+
public List<string> Errors { get; set; } = new();
8+
9+
public static OperationResult SuccessResult(string message = "Operation completed successfully")
10+
{
11+
return new OperationResult { Success = true, Message = message };
12+
}
13+
14+
public static OperationResult FailureResult(string message, List<string>? errors = null)
15+
{
16+
return new OperationResult
17+
{
18+
Success = false,
19+
Message = message,
20+
Errors = errors ?? new List<string>()
21+
};
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)