Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61172c6139 |
@@ -1,6 +1,4 @@
|
|||||||
using LiteCharms.Features.Models;
|
using LiteCharms.Features.Shop.Notifications;
|
||||||
using LiteCharms.Features.Shop.Notifications;
|
|
||||||
using LiteCharms.Features.Shop.Notifications.Events;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Tests;
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
@@ -34,31 +32,4 @@ public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper
|
|||||||
|
|
||||||
foreach (var error in createResult.Errors) output.WriteLine(error.Message);
|
foreach (var error in createResult.Errors) output.WriteLine(error.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetNotifications_ShouldReturn_AllNotifications()
|
|
||||||
{
|
|
||||||
DateRange range = new()
|
|
||||||
{
|
|
||||||
From = DateOnly.FromDateTime(new DateTime(2026, 04, 01, 0, 0, 0, DateTimeKind.Utc)),
|
|
||||||
To = DateOnly.FromDateTime(DateTime.UtcNow),
|
|
||||||
MaxRecords = 10
|
|
||||||
};
|
|
||||||
|
|
||||||
var getResult = await notificationService.GetNotificationsAsync(range);
|
|
||||||
|
|
||||||
Assert.True(getResult.IsSuccess);
|
|
||||||
|
|
||||||
foreach (var error in getResult.Errors) output.WriteLine(error.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ProcessEmailNotificationsEvent_ShouldSucceed()
|
|
||||||
{
|
|
||||||
var notification = ProcessEmailNotificationsEvent.Create();
|
|
||||||
|
|
||||||
await fixture.Mediator.Publish(notification);
|
|
||||||
|
|
||||||
Assert.True(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,8 @@ public class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
|||||||
|
|
||||||
var bodyBuilder = new BodyBuilder();
|
var bodyBuilder = new BodyBuilder();
|
||||||
|
|
||||||
if (message.Body!.Properties.HasAttachments)
|
foreach (var attachment in message.Body?.Attachments!)
|
||||||
foreach (var attachment in message.Body?.Attachments!)
|
bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken);
|
||||||
bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken);
|
|
||||||
|
|
||||||
if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message;
|
if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message;
|
||||||
if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message;
|
if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public static class Quartz
|
|||||||
storage.UseClustering(cluster =>
|
storage.UseClustering(cluster =>
|
||||||
{
|
{
|
||||||
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
||||||
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
|
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -62,8 +62,6 @@ public static class Quartz
|
|||||||
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
|
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
|
||||||
config.UseTimeZoneConverter();
|
config.UseTimeZoneConverter();
|
||||||
|
|
||||||
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString());
|
|
||||||
|
|
||||||
config.UsePersistentStore(storage =>
|
config.UsePersistentStore(storage =>
|
||||||
{
|
{
|
||||||
storage.PerformSchemaValidation = false;
|
storage.PerformSchemaValidation = false;
|
||||||
@@ -76,7 +74,7 @@ public static class Quartz
|
|||||||
storage.UseClustering(cluster =>
|
storage.UseClustering(cluster =>
|
||||||
{
|
{
|
||||||
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
||||||
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
|
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
using Amazon.Runtime;
|
|
||||||
using LiteCharms.Features.S3;
|
|
||||||
using LiteCharms.Features.S3.Configuration;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Extensions;
|
|
||||||
|
|
||||||
public static class S3
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
var optionsSection = configuration.GetSection(nameof(S3Settings));
|
|
||||||
services.Configure<S3Settings>(optionsSection);
|
|
||||||
|
|
||||||
var options = optionsSection.Get<S3Settings>()
|
|
||||||
?? throw new InvalidOperationException("S3 configuration section is missing.");
|
|
||||||
|
|
||||||
var credentials = new BasicAWSCredentials(options.AccessKey, options.SecretKey);
|
|
||||||
|
|
||||||
var s3Config = new AmazonS3Config
|
|
||||||
{
|
|
||||||
ServiceURL = options.ServiceUrl,
|
|
||||||
AuthenticationRegion = options.Region,
|
|
||||||
ForcePathStyle = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
services.AddSingleton<IAmazonS3>(new AmazonS3Client(credentials, s3Config));
|
|
||||||
services.AddScoped<S3Service>();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -129,16 +129,6 @@
|
|||||||
<Using Include="Mediator" />
|
<Using Include="Mediator" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Amazon S3 SDK -->
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.3.40" />
|
|
||||||
<PackageReference Include="AWSSDK.S3" Version="4.0.23.3" />
|
|
||||||
|
|
||||||
<!-- global Usings -->
|
|
||||||
<Using Include="Amazon.S3" />
|
|
||||||
<Using Include="Amazon.S3.Model" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra
|
|||||||
|
|
||||||
var trigger = global::Quartz.TriggerBuilder.Create()
|
var trigger = global::Quartz.TriggerBuilder.Create()
|
||||||
.WithIdentity(triggerKey)
|
.WithIdentity(triggerKey)
|
||||||
.WithDescription($"Scheduled via Main Job at {now:g}")
|
.WithDescription($"Scheduled via Main Job at {now:g} UTC")
|
||||||
.WithCronSchedule(cronExpression, cron => cron
|
.WithCronSchedule(cronExpression, cron => cron
|
||||||
.WithMisfireHandlingInstructionIgnoreMisfires())
|
.WithMisfireHandlingInstructionFireAndProceed())
|
||||||
.StartAt(now)
|
.StartAt(now)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -10,28 +10,16 @@ public class MediatorJob<TNotification>(IMediator mediator) : IJob where TNotifi
|
|||||||
{
|
{
|
||||||
var data = context.MergedJobDataMap["Payload"] as string;
|
var data = context.MergedJobDataMap["Payload"] as string;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(data))
|
if (string.IsNullOrWhiteSpace(data)) return;
|
||||||
{
|
|
||||||
Trace.WriteLine("Job Payload missing, job ended");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var notification = JsonSerializer.Deserialize<TNotification>(data);
|
var notification = JsonSerializer.Deserialize<TNotification>(data);
|
||||||
|
|
||||||
if (notification is null)
|
if (notification is null) return;
|
||||||
{
|
|
||||||
Trace.WriteLine("Notification could not be JSon converted from data string, job ended");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(TNotification).Name}");
|
using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(TNotification).Name}");
|
||||||
|
|
||||||
activity?.SetTag("event.correlation_id", notification.CorrelationId);
|
activity?.SetTag("event.correlation_id", notification.CorrelationId);
|
||||||
|
|
||||||
await mediator.Publish(notification, context.CancellationToken);
|
await mediator.Publish(notification, context.CancellationToken);
|
||||||
|
|
||||||
Trace.WriteLine("Job published");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace LiteCharms.Features.S3.Configuration;
|
|
||||||
|
|
||||||
public class S3Settings
|
|
||||||
{
|
|
||||||
public string? ServiceUrl { get; set; }
|
|
||||||
|
|
||||||
public string? AccessKey { get; set; }
|
|
||||||
|
|
||||||
public string? SecretKey { get; set; }
|
|
||||||
|
|
||||||
public string? BucketName { get; set; }
|
|
||||||
|
|
||||||
public string? Region { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
namespace LiteCharms.Features.S3;
|
|
||||||
|
|
||||||
public class S3Service(IAmazonS3 amazonS3)
|
|
||||||
{
|
|
||||||
public async Task<Result<string>> UploadFileAsync(string bucketName, string fileName, Stream fileStream, string contentType, string cdnBaseUrl, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var putRequest = new PutObjectRequest
|
|
||||||
{
|
|
||||||
BucketName = bucketName,
|
|
||||||
Key = fileName,
|
|
||||||
InputStream = fileStream,
|
|
||||||
ContentType = contentType
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await amazonS3.PutObjectAsync(putRequest, cancellationToken);
|
|
||||||
|
|
||||||
return response.HttpStatusCode != System.Net.HttpStatusCode.OK
|
|
||||||
? Result.Fail<string>($"Failed to upload {fileName} to S3.")
|
|
||||||
: Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-12
@@ -1,5 +1,4 @@
|
|||||||
using k8s.KubeConfigModels;
|
using LiteCharms.Features.Email;
|
||||||
using LiteCharms.Features.Email;
|
|
||||||
using LiteCharms.Features.Shop.Notifications.Models;
|
using LiteCharms.Features.Shop.Notifications.Models;
|
||||||
using LiteCharms.Features.Shop.Postgres;
|
using LiteCharms.Features.Shop.Postgres;
|
||||||
|
|
||||||
@@ -16,20 +15,17 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbConte
|
|||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
if (emailService.Status != EmailStatuses.Connected)
|
|
||||||
await emailService.ConnectAsync(cancellationToken);
|
|
||||||
|
|
||||||
var notifications = await context.Notifications
|
var notifications = await context.Notifications
|
||||||
.OrderByDescending(o => o.CreatedAt)
|
.OrderByDescending(o => o.CreatedAt)
|
||||||
.ThenBy(o => o.Priority)
|
.ThenBy(o => o.Priority)
|
||||||
.Where(n => n.Platform == NotificationPlatforms.Email &&
|
.Where(n => n.CorrelationIdType == CorrelationIdTypes.Email)
|
||||||
n.Direction == NotificationDirection.Outgoing && n.Processed == false)
|
.Where(n => n.Direction == NotificationDirection.Outgoing)
|
||||||
.Take(message.MaxRecords)
|
.Take(message.MaxRecords)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
foreach (var notification in notifications)
|
foreach (var notification in notifications)
|
||||||
{
|
{
|
||||||
if (dropBatch) break;
|
if (dropBatch || cancellationToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
var sendResult = await SendEmailAsync(notification,emailService, cancellationToken);
|
var sendResult = await SendEmailAsync(notification,emailService, cancellationToken);
|
||||||
|
|
||||||
@@ -56,10 +52,6 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbConte
|
|||||||
{
|
{
|
||||||
logger.LogError(ex, ex.Message);
|
logger.LogError(ex, ex.Message);
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
await emailService.DisconnectAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result> SendEmailAsync(Notification notification, EmailService service, CancellationToken cancellationToken = default)
|
private async Task<Result> SendEmailAsync(Notification notification, EmailService service, CancellationToken cancellationToken = default)
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ public class ProcessEmailNotificationsEvent : EventBase, IEvent
|
|||||||
|
|
||||||
public int MaxRecords { get; set; }
|
public int MaxRecords { get; set; }
|
||||||
|
|
||||||
public ProcessEmailNotificationsEvent() { MaxRecords = 1000; }
|
|
||||||
|
|
||||||
private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords;
|
private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords;
|
||||||
|
|
||||||
public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords);
|
public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords);
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ public class NotificationService(IDbContextFactory<ShopDbContext> contextFactory
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
|
var fromDate = range.From.ToDateTime(TimeOnly.MinValue);
|
||||||
var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
|
var toDate = range.To.ToDateTime(TimeOnly.MaxValue);
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -96,9 +96,12 @@ public class NotificationService(IDbContextFactory<ShopDbContext> contextFactory
|
|||||||
return Result.Fail(new Error($"Notification with id {request.NotificationId} not found."));
|
return Result.Fail(new Error($"Notification with id {request.NotificationId} not found."));
|
||||||
|
|
||||||
notification.Processed = request.Processed;
|
notification.Processed = request.Processed;
|
||||||
notification.UpdatedAt = DateTime.UtcNow;
|
|
||||||
notification.HasError = request.HasError;
|
if (request.HasError)
|
||||||
notification.Errors = request.Errors;
|
{
|
||||||
|
notification.HasError = request.HasError;
|
||||||
|
notification.Errors = request.Errors;
|
||||||
|
}
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
? Result.Ok()
|
? Result.Ok()
|
||||||
|
|||||||
Reference in New Issue
Block a user