diff --git a/MessagingService.BusinessLogic.Tests/DomainEventHanders/DomainEventHandlerResolverTests.cs b/MessagingService.BusinessLogic.Tests/DomainEventHanders/DomainEventHandlerResolverTests.cs new file mode 100644 index 0000000..781352d --- /dev/null +++ b/MessagingService.BusinessLogic.Tests/DomainEventHanders/DomainEventHandlerResolverTests.cs @@ -0,0 +1,101 @@ +namespace MessagingService.BusinessLogic.Tests.DomainEventHanders +{ + using System; + using System.Collections.Generic; + using System.Linq; + using EmailMessage.DomainEvents; + using EventHandling; + using Moq; + using Shouldly; + using Testing; + using Xunit; + + public class DomainEventHandlerResolverTests + { + [Fact] + public void DomainEventHandlerResolver_CanBeCreated_IsCreated() + { + Dictionary eventHandlerConfiguration = new Dictionary(); + + eventHandlerConfiguration.Add("TestEventType1", new String[] { "MessagingService.BusinessLogic.EventHandling.EmailDomainEventHandler" }); + + Mock domainEventHandler = new Mock(); + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + DomainEventHandlerResolver resolver = new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc); + + resolver.ShouldNotBeNull(); + } + + [Fact] + public void DomainEventHandlerResolver_CanBeCreated_InvalidEventHandlerType_ErrorThrown() + { + Dictionary eventHandlerConfiguration = new Dictionary(); + + eventHandlerConfiguration.Add("TestEventType1", new String[] { "MessagingService.BusinessLogic.EventHandling.NonExistantDomainEventHandler" }); + + Mock domainEventHandler = new Mock(); + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + + Should.Throw(() => new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc)); + } + + [Fact] + public void DomainEventHandlerResolver_GetDomainEventHandlers_ResponseReceivedFromProviderEvent_EventHandlersReturned() + { + String handlerTypeName = "MessagingService.BusinessLogic.EventHandling.EmailDomainEventHandler"; + Dictionary eventHandlerConfiguration = new Dictionary(); + + ResponseReceivedFromProviderEvent responseReceivedFromProviderEvent = TestData.ResponseReceivedFromProviderEvent; + + eventHandlerConfiguration.Add(responseReceivedFromProviderEvent.GetType().FullName, new String[] { handlerTypeName }); + + Mock domainEventHandler = new Mock(); + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + + DomainEventHandlerResolver resolver = new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc); + + List handlers = resolver.GetDomainEventHandlers(responseReceivedFromProviderEvent); + + handlers.ShouldNotBeNull(); + handlers.Any().ShouldBeTrue(); + handlers.Count.ShouldBe(1); + } + + [Fact] + public void DomainEventHandlerResolver_GetDomainEventHandlers_ResponseReceivedFromProviderEvent_EventNotConfigured_EventHandlersReturned() + { + String handlerTypeName = "MessagingService.BusinessLogic.EventHandling.EmailDomainEventHandler"; + Dictionary eventHandlerConfiguration = new Dictionary(); + + ResponseReceivedFromProviderEvent responseReceivedFromProviderEvent = TestData.ResponseReceivedFromProviderEvent; + + eventHandlerConfiguration.Add("RandomEvent", new String[] { handlerTypeName }); + Mock domainEventHandler = new Mock(); + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + + DomainEventHandlerResolver resolver = new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc); + + List handlers = resolver.GetDomainEventHandlers(responseReceivedFromProviderEvent); + + handlers.ShouldBeNull(); + } + + [Fact] + public void DomainEventHandlerResolver_GetDomainEventHandlers_ResponseReceivedFromProviderEvent_NoHandlersConfigured_EventHandlersReturned() + { + Dictionary eventHandlerConfiguration = new Dictionary(); + + ResponseReceivedFromProviderEvent responseReceivedFromProviderEvent = TestData.ResponseReceivedFromProviderEvent; + Mock domainEventHandler = new Mock(); + + Func createDomainEventHandlerFunc = (type) => { return domainEventHandler.Object; }; + + DomainEventHandlerResolver resolver = new DomainEventHandlerResolver(eventHandlerConfiguration, createDomainEventHandlerFunc); + + List handlers = resolver.GetDomainEventHandlers(responseReceivedFromProviderEvent); + + handlers.ShouldBeNull(); + } + + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic.Tests/DomainEventHanders/EmailDomainEventHandlerTests.cs b/MessagingService.BusinessLogic.Tests/DomainEventHanders/EmailDomainEventHandlerTests.cs new file mode 100644 index 0000000..59ddaad --- /dev/null +++ b/MessagingService.BusinessLogic.Tests/DomainEventHanders/EmailDomainEventHandlerTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Text; + +namespace MessagingService.BusinessLogic.Tests.DomainEventHanders +{ + using System.Threading; + using BusinessLogic.Services.EmailServices; + using EmailMessageAggregate; + using EventHandling; + using Moq; + using Shared.EventStore.EventStore; + using System.Threading.Tasks; + using Testing; + using Xunit; + + public class EmailDomainEventHandlerTests + { + [Fact] + public async Task EmailDomainEventHandler_Handle_ResponseReceivedFromProviderEvent_Delivered_EventIsHandled() + { + Mock> aggregateRepository = new Mock>(); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetSentEmailAggregate); + Mock emailServiceProxy = new Mock(); + emailServiceProxy.Setup(e => e.GetMessageStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.MessageStatusResponseDelivered); + + EmailDomainEventHandler emailDomainEventHandler = new EmailDomainEventHandler(aggregateRepository.Object, + emailServiceProxy.Object); + + await emailDomainEventHandler.Handle(TestData.ResponseReceivedFromProviderEvent, CancellationToken.None); + } + + [Fact] + public async Task EmailDomainEventHandler_Handle_ResponseReceivedFromProviderEvent_Failed_EventIsHandled() + { + Mock> aggregateRepository = new Mock>(); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetSentEmailAggregate); + Mock emailServiceProxy = new Mock(); + emailServiceProxy.Setup(e => e.GetMessageStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.MessageStatusResponseFailed); + + EmailDomainEventHandler emailDomainEventHandler = new EmailDomainEventHandler(aggregateRepository.Object, + emailServiceProxy.Object); + + await emailDomainEventHandler.Handle(TestData.ResponseReceivedFromProviderEvent, CancellationToken.None); + } + + [Fact] + public async Task EmailDomainEventHandler_Handle_ResponseReceivedFromProviderEvent_Rejected_EventIsHandled() + { + Mock> aggregateRepository = new Mock>(); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetSentEmailAggregate); + Mock emailServiceProxy = new Mock(); + emailServiceProxy.Setup(e => e.GetMessageStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.MessageStatusResponseRejected); + + EmailDomainEventHandler emailDomainEventHandler = new EmailDomainEventHandler(aggregateRepository.Object, + emailServiceProxy.Object); + + await emailDomainEventHandler.Handle(TestData.ResponseReceivedFromProviderEvent, CancellationToken.None); + } + + [Fact] + public async Task EmailDomainEventHandler_Handle_ResponseReceivedFromProviderEvent_Bounced_EventIsHandled() + { + Mock> aggregateRepository = new Mock>(); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetSentEmailAggregate); + Mock emailServiceProxy = new Mock(); + emailServiceProxy.Setup(e => e.GetMessageStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.MessageStatusResponseBounced); + + EmailDomainEventHandler emailDomainEventHandler = new EmailDomainEventHandler(aggregateRepository.Object, + emailServiceProxy.Object); + + await emailDomainEventHandler.Handle(TestData.ResponseReceivedFromProviderEvent, CancellationToken.None); + } + + [Fact] + public async Task EmailDomainEventHandler_Handle_ResponseReceivedFromProviderEvent_Spam_EventIsHandled() + { + Mock> aggregateRepository = new Mock>(); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetSentEmailAggregate); + Mock emailServiceProxy = new Mock(); + emailServiceProxy.Setup(e => e.GetMessageStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.MessageStatusResponseSpam); + + EmailDomainEventHandler emailDomainEventHandler = new EmailDomainEventHandler(aggregateRepository.Object, + emailServiceProxy.Object); + + await emailDomainEventHandler.Handle(TestData.ResponseReceivedFromProviderEvent, CancellationToken.None); + } + + [Fact] + public async Task EmailDomainEventHandler_Handle_ResponseReceivedFromProviderEvent_Unknown_EventIsHandled() + { + Mock> aggregateRepository = new Mock>(); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetSentEmailAggregate); + Mock emailServiceProxy = new Mock(); + emailServiceProxy.Setup(e => e.GetMessageStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(TestData.MessageStatusResponseUnknown); + + EmailDomainEventHandler emailDomainEventHandler = new EmailDomainEventHandler(aggregateRepository.Object, + emailServiceProxy.Object); + + await emailDomainEventHandler.Handle(TestData.ResponseReceivedFromProviderEvent, CancellationToken.None); + } + } +} diff --git a/MessagingService.BusinessLogic.Tests/Services/EmailDomainServiceTests.cs b/MessagingService.BusinessLogic.Tests/Services/EmailDomainServiceTests.cs index 9277016..cb9c088 100644 --- a/MessagingService.BusinessLogic.Tests/Services/EmailDomainServiceTests.cs +++ b/MessagingService.BusinessLogic.Tests/Services/EmailDomainServiceTests.cs @@ -17,7 +17,7 @@ namespace MessagingService.BusinessLogic.Tests.Services public class EmailDomainServiceTests { [Fact] - public async Task TransactionDomainService_ProcessLogonTransaction_TransactionIsProcessed() + public async Task EmailDomainService_SendEmailMessage_MessageSent() { Mock> aggregateRepository = new Mock>(); aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetEmptyEmailAggregate()); @@ -30,11 +30,7 @@ public async Task TransactionDomainService_ProcessLogonTransaction_TransactionIs It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(TestData.SuccessfulEmailServiceProxyResponse); - //IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); - //ConfigurationReader.Initialise(configurationRoot); - - //Logger.Initialise(NullLogger.Instance); - + EmailDomainService emailDomainService = new EmailDomainService(aggregateRepository.Object, emailServiceProxy.Object); diff --git a/MessagingService.BusinessLogic/EventHandling/DomainEventHandlerResolver.cs b/MessagingService.BusinessLogic/EventHandling/DomainEventHandlerResolver.cs new file mode 100644 index 0000000..393f014 --- /dev/null +++ b/MessagingService.BusinessLogic/EventHandling/DomainEventHandlerResolver.cs @@ -0,0 +1,99 @@ +namespace MessagingService.BusinessLogic.EventHandling +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Shared.DomainDrivenDesign.EventSourcing; + + public class DomainEventHandlerResolver : IDomainEventHandlerResolver + { + #region Fields + + /// + /// The domain event handlers + /// + private readonly Dictionary DomainEventHandlers; + + /// + /// The event handler configuration + /// + private readonly Dictionary EventHandlerConfiguration; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The event handler configuration. + public DomainEventHandlerResolver(Dictionary eventHandlerConfiguration, Func createEventHandlerResolver) + { + this.EventHandlerConfiguration = eventHandlerConfiguration; + + this.DomainEventHandlers = new Dictionary(); + + List handlers = new List(); + + // Precreate the Event Handlers here + foreach (KeyValuePair handlerConfig in eventHandlerConfiguration) + { + handlers.AddRange(handlerConfig.Value); + } + + IEnumerable distinctHandlers = handlers.Distinct(); + + foreach (String handlerTypeString in distinctHandlers) + { + Type handlerType = Type.GetType(handlerTypeString); + + if (handlerType == null) + { + throw new NotSupportedException("Event handler configuration is not for a valid type"); + } + + IDomainEventHandler eventHandler = createEventHandlerResolver(handlerType); + this.DomainEventHandlers.Add(handlerTypeString, eventHandler); + } + } + + #endregion + + #region Methods + + /// + /// Gets the domain event handlers. + /// + /// The domain event. + /// + public List GetDomainEventHandlers(DomainEvent domainEvent) + { + // Get the type of the event passed in + String typeString = domainEvent.GetType().FullName; + + // Lookup the list + Boolean eventIsConfigured = this.EventHandlerConfiguration.ContainsKey(typeString); + + if (!eventIsConfigured) + { + // No handlers setup, return null and let the caller decide what to do next + return null; + } + + String[] handlers = this.EventHandlerConfiguration[typeString]; + + List handlersToReturn = new List(); + + foreach (String handler in handlers) + { + List> foundHandlers = this.DomainEventHandlers.Where(h => h.Key == handler).ToList(); + + handlersToReturn.AddRange(foundHandlers.Select(x => x.Value)); + } + + return handlersToReturn; + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/EventHandling/EmailDomainEventHandler.cs b/MessagingService.BusinessLogic/EventHandling/EmailDomainEventHandler.cs new file mode 100644 index 0000000..5f3e5dc --- /dev/null +++ b/MessagingService.BusinessLogic/EventHandling/EmailDomainEventHandler.cs @@ -0,0 +1,107 @@ +namespace MessagingService.BusinessLogic.EventHandling +{ + using System.Threading; + using System.Threading.Tasks; + using EmailMessage.DomainEvents; + using EmailMessageAggregate; + using Services.EmailServices; + using Shared.DomainDrivenDesign.EventSourcing; + using Shared.EventStore.EventStore; + using MessageStatus = Services.EmailServices.MessageStatus; + + /// + /// + /// + /// + public class EmailDomainEventHandler : IDomainEventHandler + { + #region Fields + + /// + /// The aggregate repository + /// + private readonly IAggregateRepository AggregateRepository; + + /// + /// The email service proxy + /// + private readonly IEmailServiceProxy EmailServiceProxy; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate repository. + /// The email service proxy. + public EmailDomainEventHandler(IAggregateRepository aggregateRepository, + IEmailServiceProxy emailServiceProxy) + { + this.AggregateRepository = aggregateRepository; + this.EmailServiceProxy = emailServiceProxy; + } + + #endregion + + #region Methods + + /// + /// Handles the specified domain event. + /// + /// The domain event. + /// The cancellation token. + public async Task Handle(DomainEvent domainEvent, + CancellationToken cancellationToken) + { + await this.HandleSpecificDomainEvent((dynamic)domainEvent, cancellationToken); + } + + /// + /// Handles the specific domain event. + /// + /// The domain event. + /// The cancellation token. + private async Task HandleSpecificDomainEvent(ResponseReceivedFromProviderEvent domainEvent, + CancellationToken cancellationToken) + { + EmailAggregate emailAggregate = await this.AggregateRepository.GetLatestVersion(domainEvent.MessageId, cancellationToken); + + // Update the aggregate with the status request information + + // Get the message status from the provider + MessageStatusResponse messageStatus = await this.EmailServiceProxy.GetMessageStatus(domainEvent.ProviderEmailReference, + domainEvent.EventCreatedDateTime, + domainEvent.EventCreatedDateTime, + cancellationToken); + + // Update the aggregate with the response + switch (messageStatus.MessageStatus) + { + case MessageStatus.Failed: + emailAggregate.MarkMessageAsFailed(messageStatus.ProviderStatusDescription, messageStatus.Timestamp); + break; + case MessageStatus.Rejected: + emailAggregate.MarkMessageAsRejected(messageStatus.ProviderStatusDescription, messageStatus.Timestamp); + break; + case MessageStatus.Bounced: + emailAggregate.MarkMessageAsBounced(messageStatus.ProviderStatusDescription, messageStatus.Timestamp); + break; + case MessageStatus.Spam: + emailAggregate.MarkMessageAsSpam(messageStatus.ProviderStatusDescription, messageStatus.Timestamp); + break; + case MessageStatus.Delivered: + emailAggregate.MarkMessageAsDelivered(messageStatus.ProviderStatusDescription, messageStatus.Timestamp); + break; + case MessageStatus.Unknown: + break; + } + + // Save the changes + await this.AggregateRepository.SaveChanges(emailAggregate, cancellationToken); + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/EventHandling/IDomainEventHandler.cs b/MessagingService.BusinessLogic/EventHandling/IDomainEventHandler.cs new file mode 100644 index 0000000..494f396 --- /dev/null +++ b/MessagingService.BusinessLogic/EventHandling/IDomainEventHandler.cs @@ -0,0 +1,22 @@ +namespace MessagingService.BusinessLogic.EventHandling +{ + using System.Threading; + using System.Threading.Tasks; + using Shared.DomainDrivenDesign.EventSourcing; + + public interface IDomainEventHandler + { + #region Methods + + /// + /// Handles the specified domain event. + /// + /// The domain event. + /// The cancellation token. + /// + Task Handle(DomainEvent domainEvent, + CancellationToken cancellationToken); + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/EventHandling/IDomainEventHandlerResolver.cs b/MessagingService.BusinessLogic/EventHandling/IDomainEventHandlerResolver.cs new file mode 100644 index 0000000..62db6f3 --- /dev/null +++ b/MessagingService.BusinessLogic/EventHandling/IDomainEventHandlerResolver.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Text; + +namespace MessagingService.BusinessLogic.EventHandling +{ + using Shared.DomainDrivenDesign.EventSourcing; + + public interface IDomainEventHandlerResolver + { + #region Methods + + /// + /// Gets the domain event handlers. + /// + /// The domain event. + /// + List GetDomainEventHandlers(DomainEvent domainEvent); + + #endregion + } +} diff --git a/MessagingService.BusinessLogic/Services/EmailDomainService.cs b/MessagingService.BusinessLogic/Services/EmailDomainService.cs index dd7cdac..f9609e4 100644 --- a/MessagingService.BusinessLogic/Services/EmailDomainService.cs +++ b/MessagingService.BusinessLogic/Services/EmailDomainService.cs @@ -42,7 +42,6 @@ public EmailDomainService(IAggregateRepository emailAggregateRep { this.EmailAggregateRepository = emailAggregateRepository; this.EmailServiceProxy = emailServiceProxy; - this.EmailAggregateRepository.TraceGenerated += EmailDomainService.EmailAggregateRepository_TraceGenerated; } #endregion @@ -85,41 +84,7 @@ public async Task SendEmailMessage(Guid connectionIdentifier, // Save Changes to persistance await this.EmailAggregateRepository.SaveChanges(emailAggregate, cancellationToken); } - - /// - /// Emails the aggregate repository trace generated. - /// - /// The trace. - /// The log level. - private static void EmailAggregateRepository_TraceGenerated(String trace, - LogLevel logLevel) - { - switch(logLevel) - { - case LogLevel.Critical: - Logger.LogCritical(new Exception(trace)); - break; - case LogLevel.Debug: - Logger.LogDebug(trace); - break; - case LogLevel.Error: - Logger.LogError(new Exception(trace)); - break; - case LogLevel.Information: - Logger.LogInformation(trace); - break; - case LogLevel.Trace: - Logger.LogTrace(trace); - break; - case LogLevel.Warning: - Logger.LogWarning(trace); - break; - default: - Logger.LogInformation(trace); - break; - } - } - + #endregion } } \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/IEmailServiceProxy.cs b/MessagingService.BusinessLogic/Services/EmailServices/IEmailServiceProxy.cs index 3a8780e..cd43634 100644 --- a/MessagingService.BusinessLogic/Services/EmailServices/IEmailServiceProxy.cs +++ b/MessagingService.BusinessLogic/Services/EmailServices/IEmailServiceProxy.cs @@ -28,6 +28,19 @@ Task SendEmail(Guid messageId, Boolean isHtml, CancellationToken cancellationToken); + /// + /// Gets the message status. + /// + /// The provider reference. + /// The start date. + /// The end date. + /// The cancellation token. + /// + Task GetMessageStatus(String providerReference, + DateTime startDate, + DateTime endDate, + CancellationToken cancellationToken); + #endregion } } \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/IntegrationTest/IntegrationTestEmailServiceProxy.cs b/MessagingService.BusinessLogic/Services/EmailServices/IntegrationTest/IntegrationTestEmailServiceProxy.cs index c676bdb..4abb705 100644 --- a/MessagingService.BusinessLogic/Services/EmailServices/IntegrationTest/IntegrationTestEmailServiceProxy.cs +++ b/MessagingService.BusinessLogic/Services/EmailServices/IntegrationTest/IntegrationTestEmailServiceProxy.cs @@ -1,19 +1,42 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace MessagingService.Service.Services.Email.IntegrationTest +namespace MessagingService.Service.Services.Email.IntegrationTest { + using System; + using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; - using BusinessLogic.Requests; + using System.Net; + using System.Threading; + using System.Threading.Tasks; using BusinessLogic.Services.EmailServices; + /// + /// + /// + /// [ExcludeFromCodeCoverage] public class IntegrationTestEmailServiceProxy : IEmailServiceProxy { + #region Methods + + /// + /// Gets the message status. + /// + /// The provider reference. + /// The start date. + /// The end date. + /// The cancellation token. + /// + public async Task GetMessageStatus(String providerReference, + DateTime startDate, + DateTime endDate, + CancellationToken cancellationToken) + { + return new MessageStatusResponse + { + MessageStatus = MessageStatus.Delivered, + ProviderStatusDescription = "delivered" + }; + } + /// /// Sends the email. /// @@ -30,16 +53,19 @@ public async Task SendEmail(Guid messageId, List toAddresses, String subject, String body, - Boolean isHtml, CancellationToken cancellationToken) + Boolean isHtml, + CancellationToken cancellationToken) { return new EmailServiceProxyResponse - { - RequestIdentifier = "requestid", - EmailIdentifier = "emailid", - ApiStatusCode = HttpStatusCode.OK, - Error = String.Empty, - ErrorCode = String.Empty - }; + { + RequestIdentifier = "requestid", + EmailIdentifier = "emailid", + ApiStatusCode = HttpStatusCode.OK, + Error = string.Empty, + ErrorCode = string.Empty + }; } + + #endregion } -} +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/MessageStatus.cs b/MessagingService.BusinessLogic/Services/EmailServices/MessageStatus.cs new file mode 100644 index 0000000..ce22f27 --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/MessageStatus.cs @@ -0,0 +1,14 @@ +namespace MessagingService.BusinessLogic.Services.EmailServices +{ + using System.Diagnostics.CodeAnalysis; + + public enum MessageStatus + { + Delivered, + Failed, + Bounced, + Rejected, + Spam, + Unknown + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/MessageStatusResponse.cs b/MessagingService.BusinessLogic/Services/EmailServices/MessageStatusResponse.cs new file mode 100644 index 0000000..99795e2 --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/MessageStatusResponse.cs @@ -0,0 +1,49 @@ +namespace MessagingService.BusinessLogic.Services.EmailServices +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Net; + + /// + /// + /// + [ExcludeFromCodeCoverage] + public class MessageStatusResponse + { + #region Properties + + /// + /// Gets or sets the API status code. + /// + /// + /// The API status code. + /// + public HttpStatusCode ApiStatusCode { get; set; } + + /// + /// Gets or sets the message status. + /// + /// + /// The message status. + /// + public MessageStatus MessageStatus { get; set; } + + /// + /// Gets or sets the provider status description. + /// + /// + /// The provider status description. + /// + public String ProviderStatusDescription { get; set; } + + /// + /// Gets or sets the timestamp. + /// + /// + /// The timestamp. + /// + public DateTime Timestamp { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Data.cs b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Data.cs new file mode 100644 index 0000000..0b72bdb --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Data.cs @@ -0,0 +1,32 @@ +namespace MessagingService.BusinessLogic.Services.EmailServices.Smtp2Go +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + + /// + /// + /// + [ExcludeFromCodeCoverage] + public class Data + { + /// + /// Gets or sets the count. + /// + /// + /// The count. + /// + [JsonProperty("count")] + public Int32 Count { get; set; } + + /// + /// Gets or sets the email details. + /// + /// + /// The email details. + /// + [JsonProperty("emails")] + public List EmailDetails { get; set; } + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/EmailDetails.cs b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/EmailDetails.cs new file mode 100644 index 0000000..c5a8f45 --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/EmailDetails.cs @@ -0,0 +1,67 @@ +namespace MessagingService.BusinessLogic.Services.EmailServices.Smtp2Go +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + + /// + /// + /// + [ExcludeFromCodeCoverage] + public class EmailDetails + { + /// + /// Gets or sets the subject. + /// + /// + /// The subject. + /// + [JsonProperty("subject")] + public String Subject { get; set; } + + /// + /// Gets or sets the delivered at. + /// + /// + /// The delivered at. + /// + [JsonProperty("delivered_at")] + public DateTime DeliveredAt { get; set; } + + /// + /// Gets or sets the email status date. + /// + /// + /// The email status date. + /// + [JsonProperty("email_ts")] + public DateTime EmailStatusDate { get; set; } + + /// + /// Gets or sets the process status. + /// + /// + /// The process status. + /// + [JsonProperty("process_status")] + public String ProcessStatus { get; set; } + + /// + /// Gets or sets the email identifier. + /// + /// + /// The email identifier. + /// + [JsonProperty("email_id")] + public String EmailId { get; set; } + + /// + /// Gets or sets the status. + /// + /// + /// The status. + /// + [JsonProperty("status")] + public String Status { get; set; } + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoEmailSearchRequest.cs b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoEmailSearchRequest.cs new file mode 100644 index 0000000..c00d60d --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoEmailSearchRequest.cs @@ -0,0 +1,50 @@ +namespace MessagingService.BusinessLogic.Services.EmailServices.Smtp2Go +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + + /// + /// + /// + [ExcludeFromCodeCoverage] + public class Smtp2GoEmailSearchRequest + { + /// + /// Gets or sets the email identifier. + /// + /// + /// The email identifier. + /// + [JsonProperty("email_id")] + public List EmailId { get; set; } + + /// + /// Gets or sets the API key. + /// + /// + /// The API key. + /// + [JsonProperty("api_key")] + public String ApiKey { get; set; } + + /// + /// Gets or sets the start date. + /// + /// + /// The start date. + /// + [JsonProperty("start_date")] + public String StartDate { get; set; } + + /// + /// Gets or sets the end date. + /// + /// + /// The end date. + /// + [JsonProperty("end_date")] + public String EndDate { get; set; } + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoEmailSearchResponse.cs b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoEmailSearchResponse.cs new file mode 100644 index 0000000..81763a7 --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoEmailSearchResponse.cs @@ -0,0 +1,35 @@ +namespace MessagingService.BusinessLogic.Services.EmailServices.Smtp2Go +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + + /// + /// + /// + [ExcludeFromCodeCoverage] + public class Smtp2GoEmailSearchResponse + { + #region Properties + + /// + /// Gets or sets the data. + /// + /// + /// The data. + /// + [JsonProperty("data")] + public Data Data { get; set; } + + /// + /// Gets or sets the request identifier. + /// + /// + /// The request identifier. + /// + [JsonProperty("request_id")] + public String RequestId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoProxy.cs b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoProxy.cs index f97fb69..0100633 100644 --- a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoProxy.cs +++ b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoProxy.cs @@ -2,6 +2,8 @@ { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; using System.Net.Http; using System.Text; using System.Threading; @@ -15,6 +17,7 @@ /// /// /// + [ExcludeFromCodeCoverage] public class Smtp2GoProxy : IEmailServiceProxy { #region Fields @@ -104,6 +107,93 @@ public async Task SendEmail(Guid messageId, return response; } + /// + /// Gets the message status. + /// + /// The provider reference. + /// The start date. + /// The end date. + /// The cancellation token. + /// + public async Task GetMessageStatus(String providerReference, + DateTime startDate, + DateTime endDate, + CancellationToken cancellationToken) + { + MessageStatusResponse response = null; + + Smtp2GoEmailSearchRequest apiRequest = new Smtp2GoEmailSearchRequest + { + ApiKey = ConfigurationReader.GetValue("SMTP2GoAPIKey"), + EmailId = new List{providerReference}, + StartDate = startDate.ToString("yyyy-MM-dd"), + EndDate = endDate.ToString("yyyy-MM-dd"), + }; + + String requestSerialised = JsonConvert.SerializeObject(apiRequest); + + Logger.LogDebug($"Request Message Sent to Email Provider [SMTP2Go] {requestSerialised}"); + + StringContent content = new StringContent(requestSerialised, Encoding.UTF8, "application/json"); + + using (HttpClient client = new HttpClient()) + { + client.BaseAddress = new Uri(ConfigurationReader.GetValue("SMTP2GoBaseAddress")); + + HttpResponseMessage httpResponse = await client.PostAsync("email/search", content, cancellationToken); + + Smtp2GoEmailSearchResponse apiResponse = JsonConvert.DeserializeObject(await httpResponse.Content.ReadAsStringAsync()); + + Logger.LogDebug($"Response Message Received from Email Provider [SMTP2Go] {JsonConvert.SerializeObject(apiResponse)}"); + + // Translate the Response + response = new MessageStatusResponse + { + ApiStatusCode = httpResponse.StatusCode, + MessageStatus = this.TranslateMessageStatus(apiResponse.Data.EmailDetails.Single().Status), + ProviderStatusDescription = apiResponse.Data.EmailDetails.Single().Status, + Timestamp = apiResponse.Data.EmailDetails.Single().EmailStatusDate + }; + } + + return response; + } + + private MessageStatus TranslateMessageStatus(String status) + { + MessageStatus result; + switch (status) + { + case "failed": + case "deferred": + result = MessageStatus.Failed; + break; + case "hardbounce": + case "refused": + case "softbounce": + case "returned": + result = MessageStatus.Bounced; + break; + case "delivered": + case "ok": + case "sent": + result = MessageStatus.Delivered; + break; + case "rejected": + result = MessageStatus.Rejected; + break; + case "complained": + case "spam": + result = MessageStatus.Spam; + break; + default: + result = MessageStatus.Unknown; + break; + } + + return result; + } + #endregion } } \ No newline at end of file diff --git a/MessagingService.EmailAggregate.Tests/EmailAggregateDomainEventTests.cs b/MessagingService.EmailAggregate.Tests/EmailAggregateDomainEventTests.cs index 24f732c..2b6276e 100644 --- a/MessagingService.EmailAggregate.Tests/EmailAggregateDomainEventTests.cs +++ b/MessagingService.EmailAggregate.Tests/EmailAggregateDomainEventTests.cs @@ -40,5 +40,80 @@ public void ResponseReceivedFromProviderEvent_CanBeCreated_IsCreated() requestSentToProviderEvent.ProviderRequestReference.ShouldBe(TestData.ProviderRequestReference); requestSentToProviderEvent.ProviderEmailReference.ShouldBe(TestData.ProviderEmailReference); } + + [Fact] + public void MessageDeliveredEvent_CanBeCreated_IsCreated() + { + MessageDeliveredEvent messageDeliveredEvent = + MessageDeliveredEvent.Create(TestData.MessageId, TestData.ProviderStatusDescription, TestData.DeliveredDateTime); + + messageDeliveredEvent.ShouldNotBeNull(); + messageDeliveredEvent.AggregateId.ShouldBe(TestData.MessageId); + messageDeliveredEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + messageDeliveredEvent.EventId.ShouldNotBe(Guid.Empty); + messageDeliveredEvent.MessageId.ShouldBe(TestData.MessageId); + messageDeliveredEvent.ProviderStatus.ShouldBe(TestData.ProviderStatusDescription); + messageDeliveredEvent.DeliveredDateTime.ShouldBe(TestData.DeliveredDateTime); + } + + [Fact] + public void MessageFailedEvent_CanBeCreated_IsCreated() + { + MessageFailedEvent messageFailedEvent = + MessageFailedEvent.Create(TestData.MessageId, TestData.ProviderStatusDescription, TestData.FailedDateTime); + + messageFailedEvent.ShouldNotBeNull(); + messageFailedEvent.AggregateId.ShouldBe(TestData.MessageId); + messageFailedEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + messageFailedEvent.EventId.ShouldNotBe(Guid.Empty); + messageFailedEvent.MessageId.ShouldBe(TestData.MessageId); + messageFailedEvent.ProviderStatus.ShouldBe(TestData.ProviderStatusDescription); + messageFailedEvent.FailedDateTime.ShouldBe(TestData.FailedDateTime); + } + + [Fact] + public void MessageRejectedEvent_CanBeCreated_IsCreated() + { + MessageRejectedEvent messageRejectedEvent = + MessageRejectedEvent.Create(TestData.MessageId, TestData.ProviderStatusDescription, TestData.RejectedDateTime); + + messageRejectedEvent.ShouldNotBeNull(); + messageRejectedEvent.AggregateId.ShouldBe(TestData.MessageId); + messageRejectedEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + messageRejectedEvent.EventId.ShouldNotBe(Guid.Empty); + messageRejectedEvent.MessageId.ShouldBe(TestData.MessageId); + messageRejectedEvent.ProviderStatus.ShouldBe(TestData.ProviderStatusDescription); + messageRejectedEvent.RejectedDateTime.ShouldBe(TestData.RejectedDateTime); + } + + [Fact] + public void MessageBouncedEvent_CanBeCreated_IsCreated() + { + MessageBouncedEvent messageBouncedEvent = + MessageBouncedEvent.Create(TestData.MessageId, TestData.ProviderStatusDescription, TestData.BouncedDateTime); + + messageBouncedEvent.ShouldNotBeNull(); + messageBouncedEvent.AggregateId.ShouldBe(TestData.MessageId); + messageBouncedEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + messageBouncedEvent.EventId.ShouldNotBe(Guid.Empty); + messageBouncedEvent.MessageId.ShouldBe(TestData.MessageId); + messageBouncedEvent.ProviderStatus.ShouldBe(TestData.ProviderStatusDescription); + messageBouncedEvent.BouncedDateTime.ShouldBe(TestData.BouncedDateTime); + } + + [Fact] + public void MessageMarkedAsSpamEvent_CanBeCreated_IsCreated() + { + MessageMarkedAsSpamEvent messageMarkedAsSpamEvent = + MessageMarkedAsSpamEvent.Create(TestData.MessageId, TestData.ProviderStatusDescription, TestData.SpamDateTime); + + messageMarkedAsSpamEvent.ShouldNotBeNull(); + messageMarkedAsSpamEvent.AggregateId.ShouldBe(TestData.MessageId); + messageMarkedAsSpamEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + messageMarkedAsSpamEvent.EventId.ShouldNotBe(Guid.Empty); + messageMarkedAsSpamEvent.MessageId.ShouldBe(TestData.MessageId); + messageMarkedAsSpamEvent.ProviderStatus.ShouldBe(TestData.ProviderStatusDescription); + messageMarkedAsSpamEvent.SpamDateTime.ShouldBe(TestData.SpamDateTime); + } } } \ No newline at end of file diff --git a/MessagingService.EmailAggregate.Tests/EmailAggregateTests.cs b/MessagingService.EmailAggregate.Tests/EmailAggregateTests.cs index e2172cc..a7daf6f 100644 --- a/MessagingService.EmailAggregate.Tests/EmailAggregateTests.cs +++ b/MessagingService.EmailAggregate.Tests/EmailAggregateTests.cs @@ -2,6 +2,7 @@ namespace MessagingService.EmailAggregate.Tests { + using System; using EmailMessageAggregate; using Shouldly; using Testing; @@ -41,5 +42,296 @@ public void EmailAggregate_ReceiveResponseFromProvider_ResponseReceived() emailAggregate.ProviderRequestReference.ShouldBe(TestData.ProviderRequestReference); emailAggregate.ProviderEmailReference.ShouldBe(TestData.ProviderEmailReference); } + + [Fact] + public void EmailAggregate_MarkMessageAsDelivered_MessageMarkedAsDelivered() + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + + emailAggregate.MarkMessageAsDelivered(TestData.ProviderStatusDescription, TestData.DeliveredDateTime); + + emailAggregate.MessageStatus.ShouldBe(MessageStatus.Delivered); + } + + [Theory] + [InlineData(MessageStatus.NotSet)] + [InlineData(MessageStatus.Delivered)] + [InlineData(MessageStatus.Rejected)] + [InlineData(MessageStatus.Failed)] + [InlineData(MessageStatus.Spam)] + [InlineData(MessageStatus.Bounced)] + public void EmailAggregate_MarkMessageAsDelivered_IncorrectState_ErrorThrown(MessageStatus messageStatus) + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + + switch(messageStatus) + { + case MessageStatus.Delivered: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsDelivered(TestData.ProviderStatusDescription, TestData.DeliveredDateTime); + break; + case MessageStatus.Bounced: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsBounced(TestData.ProviderStatusDescription, TestData.BouncedDateTime); + break; + case MessageStatus.Failed: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsFailed(TestData.ProviderStatusDescription, TestData.FailedDateTime); + break; + case MessageStatus.Rejected: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsRejected(TestData.ProviderStatusDescription, TestData.RejectedDateTime); + break; + case MessageStatus.Spam: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsSpam(TestData.ProviderStatusDescription, TestData.SpamDateTime); + break; + case MessageStatus.NotSet: + break; + + } + + Should.Throw(() => + { + emailAggregate.MarkMessageAsDelivered(TestData.ProviderStatusDescription, TestData.DeliveredDateTime); + }); + } + + [Fact] + public void EmailAggregate_MarkMessageAsRejected_MessageMarkedAsRejected() + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + + emailAggregate.MarkMessageAsRejected(TestData.ProviderStatusDescription, TestData.RejectedDateTime); + + emailAggregate.MessageStatus.ShouldBe(MessageStatus.Rejected); + } + + [Theory] + [InlineData(MessageStatus.NotSet)] + [InlineData(MessageStatus.Delivered)] + [InlineData(MessageStatus.Rejected)] + [InlineData(MessageStatus.Failed)] + [InlineData(MessageStatus.Spam)] + [InlineData(MessageStatus.Bounced)] + public void EmailAggregate_MarkMessageAsRejected_IncorrectState_ErrorThrown(MessageStatus messageStatus) + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + + switch (messageStatus) + { + case MessageStatus.Delivered: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsDelivered(TestData.ProviderStatusDescription, TestData.DeliveredDateTime); + break; + case MessageStatus.Bounced: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsBounced(TestData.ProviderStatusDescription, TestData.BouncedDateTime); + break; + case MessageStatus.Failed: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsFailed(TestData.ProviderStatusDescription, TestData.FailedDateTime); + break; + case MessageStatus.Rejected: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsRejected(TestData.ProviderStatusDescription, TestData.RejectedDateTime); + break; + case MessageStatus.Spam: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsSpam(TestData.ProviderStatusDescription, TestData.SpamDateTime); + break; + case MessageStatus.NotSet: + break; + } + + Should.Throw(() => + { + emailAggregate.MarkMessageAsRejected(TestData.ProviderStatusDescription, TestData.RejectedDateTime); + }); + } + + [Fact] + public void EmailAggregate_MarkMessageAsFailed_MessageMarkedAsFailed() + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + + emailAggregate.MarkMessageAsFailed(TestData.ProviderStatusDescription, TestData.FailedDateTime); + + emailAggregate.MessageStatus.ShouldBe(MessageStatus.Failed); + } + + [Theory] + [InlineData(MessageStatus.NotSet)] + [InlineData(MessageStatus.Delivered)] + [InlineData(MessageStatus.Rejected)] + [InlineData(MessageStatus.Failed)] + [InlineData(MessageStatus.Spam)] + [InlineData(MessageStatus.Bounced)] + public void EmailAggregate_MarkMessageAsFailed_IncorrectState_ErrorThrown(MessageStatus messageStatus) + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + + switch (messageStatus) + { + case MessageStatus.Delivered: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsDelivered(TestData.ProviderStatusDescription, TestData.DeliveredDateTime); + break; + case MessageStatus.Bounced: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsBounced(TestData.ProviderStatusDescription, TestData.BouncedDateTime); + break; + case MessageStatus.Failed: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsFailed(TestData.ProviderStatusDescription, TestData.FailedDateTime); + break; + case MessageStatus.Rejected: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsRejected(TestData.ProviderStatusDescription, TestData.RejectedDateTime); + break; + case MessageStatus.Spam: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsSpam(TestData.ProviderStatusDescription, TestData.SpamDateTime); + break; + case MessageStatus.NotSet: + break; + } + + Should.Throw(() => + { + emailAggregate.MarkMessageAsFailed(TestData.ProviderStatusDescription, TestData.FailedDateTime); + }); + } + + [Fact] + public void EmailAggregate_MarkMessageAsBounced_MessageMarkedAsBounced() + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + + emailAggregate.MarkMessageAsBounced(TestData.ProviderStatusDescription, TestData.BouncedDateTime); + + emailAggregate.MessageStatus.ShouldBe(MessageStatus.Bounced); + } + + [Theory] + [InlineData(MessageStatus.NotSet)] + [InlineData(MessageStatus.Delivered)] + [InlineData(MessageStatus.Rejected)] + [InlineData(MessageStatus.Failed)] + [InlineData(MessageStatus.Spam)] + [InlineData(MessageStatus.Bounced)] + public void EmailAggregate_MarkMessageAsBounced_IncorrectState_ErrorThrown(MessageStatus messageStatus) + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + + switch (messageStatus) + { + case MessageStatus.Delivered: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsDelivered(TestData.ProviderStatusDescription, TestData.DeliveredDateTime); + break; + case MessageStatus.Bounced: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsBounced(TestData.ProviderStatusDescription, TestData.BouncedDateTime); + break; + case MessageStatus.Failed: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsFailed(TestData.ProviderStatusDescription, TestData.FailedDateTime); + break; + case MessageStatus.Rejected: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsRejected(TestData.ProviderStatusDescription, TestData.RejectedDateTime); + break; + case MessageStatus.Spam: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsSpam(TestData.ProviderStatusDescription, TestData.SpamDateTime); + break; + case MessageStatus.NotSet: + break; + } + + Should.Throw(() => + { + emailAggregate.MarkMessageAsBounced(TestData.ProviderStatusDescription, TestData.BouncedDateTime); + }); + } + + [Fact] + public void EmailAggregate_MarkMessageAsSpam_MessageMarkedAsSpam() + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + + emailAggregate.MarkMessageAsSpam(TestData.ProviderStatusDescription, TestData.SpamDateTime); + + emailAggregate.MessageStatus.ShouldBe(MessageStatus.Spam); + } + + [Theory] + [InlineData(MessageStatus.NotSet)] + [InlineData(MessageStatus.Delivered)] + [InlineData(MessageStatus.Rejected)] + [InlineData(MessageStatus.Failed)] + [InlineData(MessageStatus.Spam)] + [InlineData(MessageStatus.Bounced)] + public void EmailAggregate_MarkMessageAsSpam_IncorrectState_ErrorThrown(MessageStatus messageStatus) + { + EmailAggregate emailAggregate = EmailAggregate.Create(TestData.MessageId); + + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, TestData.Body, TestData.IsHtmlTrue); + + switch (messageStatus) + { + case MessageStatus.Delivered: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsDelivered(TestData.ProviderStatusDescription, TestData.DeliveredDateTime); + break; + case MessageStatus.Bounced: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsBounced(TestData.ProviderStatusDescription, TestData.BouncedDateTime); + break; + case MessageStatus.Failed: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsFailed(TestData.ProviderStatusDescription, TestData.FailedDateTime); + break; + case MessageStatus.Rejected: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsRejected(TestData.ProviderStatusDescription, TestData.RejectedDateTime); + break; + case MessageStatus.Spam: + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + emailAggregate.MarkMessageAsSpam(TestData.ProviderStatusDescription, TestData.SpamDateTime); + break; + case MessageStatus.NotSet: + break; + } + + Should.Throw(() => + { + emailAggregate.MarkMessageAsSpam(TestData.ProviderStatusDescription, TestData.SpamDateTime); + }); + } } } diff --git a/MessagingService.EmailAggregate.Tests/MessagingService.EmailAggregate.Tests.csproj b/MessagingService.EmailAggregate.Tests/MessagingService.EmailAggregate.Tests.csproj index de1a4dc..b4e5b90 100644 --- a/MessagingService.EmailAggregate.Tests/MessagingService.EmailAggregate.Tests.csproj +++ b/MessagingService.EmailAggregate.Tests/MessagingService.EmailAggregate.Tests.csproj @@ -2,7 +2,7 @@ netcoreapp3.1 - + None false diff --git a/MessagingService.EmailMessage.DomainEvents/MessageBouncedEvent.cs b/MessagingService.EmailMessage.DomainEvents/MessageBouncedEvent.cs new file mode 100644 index 0000000..196e565 --- /dev/null +++ b/MessagingService.EmailMessage.DomainEvents/MessageBouncedEvent.cs @@ -0,0 +1,93 @@ +namespace MessagingService.EmailMessage.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + public class MessageBouncedEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public MessageBouncedEvent() + { + //We need this for serialisation, so just embrace the DDD crime + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The provider status. + /// The bounced date time. + private MessageBouncedEvent(Guid aggregateId, + Guid eventId, + String providerStatus, + DateTime bouncedDateTime) : base(aggregateId, eventId) + { + this.MessageId = aggregateId; + this.ProviderStatus = providerStatus; + this.BouncedDateTime = bouncedDateTime; + } + + #endregion + + #region Properties + + /// + /// Gets the bounced date time. + /// + /// + /// The bounced date time. + /// + [JsonProperty] + public DateTime BouncedDateTime { get; private set; } + + /// + /// Gets the message identifier. + /// + /// + /// The message identifier. + /// + [JsonProperty] + public Guid MessageId { get; private set; } + + /// + /// Gets the provider status. + /// + /// + /// The provider status. + /// + [JsonProperty] + public String ProviderStatus { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// The provider status. + /// The bounced date time. + /// + public static MessageBouncedEvent Create(Guid aggregateId, + String providerStatus, + DateTime bouncedDateTime) + { + return new MessageBouncedEvent(aggregateId, Guid.NewGuid(), providerStatus, bouncedDateTime); + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.EmailMessage.DomainEvents/MessageDeliveredEvent.cs b/MessagingService.EmailMessage.DomainEvents/MessageDeliveredEvent.cs new file mode 100644 index 0000000..68a7682 --- /dev/null +++ b/MessagingService.EmailMessage.DomainEvents/MessageDeliveredEvent.cs @@ -0,0 +1,93 @@ +namespace MessagingService.EmailMessage.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + public class MessageDeliveredEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public MessageDeliveredEvent() + { + //We need this for serialisation, so just embrace the DDD crime + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The provider status. + /// The delivered date time. + private MessageDeliveredEvent(Guid aggregateId, + Guid eventId, + String providerStatus, + DateTime deliveredDateTime) : base(aggregateId, eventId) + { + this.MessageId = aggregateId; + this.ProviderStatus = providerStatus; + this.DeliveredDateTime = deliveredDateTime; + } + + #endregion + + #region Properties + + /// + /// Gets the delivered date time. + /// + /// + /// The delivered date time. + /// + [JsonProperty] + public DateTime DeliveredDateTime { get; private set; } + + /// + /// Gets the message identifier. + /// + /// + /// The message identifier. + /// + [JsonProperty] + public Guid MessageId { get; private set; } + + /// + /// Gets the provider status. + /// + /// + /// The provider status. + /// + [JsonProperty] + public String ProviderStatus { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// The provider status. + /// The delivered date time. + /// + public static MessageDeliveredEvent Create(Guid aggregateId, + String providerStatus, + DateTime deliveredDateTime) + { + return new MessageDeliveredEvent(aggregateId, Guid.NewGuid(), providerStatus, deliveredDateTime); + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.EmailMessage.DomainEvents/MessageFailedEvent.cs b/MessagingService.EmailMessage.DomainEvents/MessageFailedEvent.cs new file mode 100644 index 0000000..d342846 --- /dev/null +++ b/MessagingService.EmailMessage.DomainEvents/MessageFailedEvent.cs @@ -0,0 +1,93 @@ +namespace MessagingService.EmailMessage.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + public class MessageFailedEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public MessageFailedEvent() + { + //We need this for serialisation, so just embrace the DDD crime + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The provider status. + /// The failed date time. + private MessageFailedEvent(Guid aggregateId, + Guid eventId, + String providerStatus, + DateTime failedDateTime) : base(aggregateId, eventId) + { + this.MessageId = aggregateId; + this.ProviderStatus = providerStatus; + this.FailedDateTime = failedDateTime; + } + + #endregion + + #region Properties + + /// + /// Gets the failed date time. + /// + /// + /// The failed date time. + /// + [JsonProperty] + public DateTime FailedDateTime { get; private set; } + + /// + /// Gets the message identifier. + /// + /// + /// The message identifier. + /// + [JsonProperty] + public Guid MessageId { get; private set; } + + /// + /// Gets the provider status. + /// + /// + /// The provider status. + /// + [JsonProperty] + public String ProviderStatus { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// The provider status. + /// The failed date time. + /// + public static MessageFailedEvent Create(Guid aggregateId, + String providerStatus, + DateTime failedDateTime) + { + return new MessageFailedEvent(aggregateId, Guid.NewGuid(), providerStatus, failedDateTime); + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.EmailMessage.DomainEvents/MessageMarkedAsSpamEvent.cs b/MessagingService.EmailMessage.DomainEvents/MessageMarkedAsSpamEvent.cs new file mode 100644 index 0000000..464b7ca --- /dev/null +++ b/MessagingService.EmailMessage.DomainEvents/MessageMarkedAsSpamEvent.cs @@ -0,0 +1,93 @@ +namespace MessagingService.EmailMessage.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + public class MessageMarkedAsSpamEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public MessageMarkedAsSpamEvent() + { + //We need this for serialisation, so just embrace the DDD crime + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The provider status. + /// The spam date time. + private MessageMarkedAsSpamEvent(Guid aggregateId, + Guid eventId, + String providerStatus, + DateTime spamDateTime) : base(aggregateId, eventId) + { + this.MessageId = aggregateId; + this.ProviderStatus = providerStatus; + this.SpamDateTime = spamDateTime; + } + + #endregion + + #region Properties + + /// + /// Gets the message identifier. + /// + /// + /// The message identifier. + /// + [JsonProperty] + public Guid MessageId { get; private set; } + + /// + /// Gets the provider status. + /// + /// + /// The provider status. + /// + [JsonProperty] + public String ProviderStatus { get; private set; } + + /// + /// Gets the spam date time. + /// + /// + /// The spam date time. + /// + [JsonProperty] + public DateTime SpamDateTime { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// The provider status. + /// The spam date time. + /// + public static MessageMarkedAsSpamEvent Create(Guid aggregateId, + String providerStatus, + DateTime spamDateTime) + { + return new MessageMarkedAsSpamEvent(aggregateId, Guid.NewGuid(), providerStatus, spamDateTime); + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.EmailMessage.DomainEvents/MessageRejectedEvent.cs b/MessagingService.EmailMessage.DomainEvents/MessageRejectedEvent.cs new file mode 100644 index 0000000..f190c8f --- /dev/null +++ b/MessagingService.EmailMessage.DomainEvents/MessageRejectedEvent.cs @@ -0,0 +1,93 @@ +namespace MessagingService.EmailMessage.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + public class MessageRejectedEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public MessageRejectedEvent() + { + //We need this for serialisation, so just embrace the DDD crime + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The provider status. + /// The rejected date time. + private MessageRejectedEvent(Guid aggregateId, + Guid eventId, + String providerStatus, + DateTime rejectedDateTime) : base(aggregateId, eventId) + { + this.MessageId = aggregateId; + this.ProviderStatus = providerStatus; + this.RejectedDateTime = rejectedDateTime; + } + + #endregion + + #region Properties + + /// + /// Gets the message identifier. + /// + /// + /// The message identifier. + /// + [JsonProperty] + public Guid MessageId { get; private set; } + + /// + /// Gets the provider status. + /// + /// + /// The provider status. + /// + [JsonProperty] + public String ProviderStatus { get; private set; } + + /// + /// Gets the rejected date time. + /// + /// + /// The rejected date time. + /// + [JsonProperty] + public DateTime RejectedDateTime { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// The provider status. + /// The rejected date time. + /// + public static MessageRejectedEvent Create(Guid aggregateId, + String providerStatus, + DateTime rejectedDateTime) + { + return new MessageRejectedEvent(aggregateId, Guid.NewGuid(), providerStatus, rejectedDateTime); + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.EmailMessageAggregate/EmailAggregate.cs b/MessagingService.EmailMessageAggregate/EmailAggregate.cs index 63fa2f8..6944c7f 100644 --- a/MessagingService.EmailMessageAggregate/EmailAggregate.cs +++ b/MessagingService.EmailMessageAggregate/EmailAggregate.cs @@ -11,6 +11,7 @@ /// /// /// + /// /// public class EmailAggregate : Aggregate { @@ -83,6 +84,14 @@ private EmailAggregate(Guid aggregateId) /// public Guid MessageId { get; } + /// + /// Gets the message status. + /// + /// + /// The message status. + /// + public MessageStatus MessageStatus { get; private set; } + /// /// Gets the provider email reference. /// @@ -121,6 +130,81 @@ public static EmailAggregate Create(Guid aggregateId) return new EmailAggregate(aggregateId); } + /// + /// Marks the message as bounced. + /// + /// The provider status. + /// The bounced date time. + public void MarkMessageAsBounced(String providerStatus, + DateTime bouncedDateTime) + { + this.CheckMessageCanBeSetToBounced(); + + MessageBouncedEvent messageBouncedEvent = MessageBouncedEvent.Create(this.AggregateId, providerStatus, bouncedDateTime); + + this.ApplyAndPend(messageBouncedEvent); + } + + /// + /// Marks the message as delivered. + /// + /// The provider status. + /// The delivered date time. + public void MarkMessageAsDelivered(String providerStatus, + DateTime deliveredDateTime) + { + this.CheckMessageCanBeSetToDelivered(); + + MessageDeliveredEvent messageDeliveredEvent = MessageDeliveredEvent.Create(this.AggregateId, providerStatus, deliveredDateTime); + + this.ApplyAndPend(messageDeliveredEvent); + } + + /// + /// Marks the message as failed. + /// + /// The provider status. + /// The failed date time. + public void MarkMessageAsFailed(String providerStatus, + DateTime failedDateTime) + { + this.CheckMessageCanBeSetToFailed(); + + MessageFailedEvent messageFailedEvent = MessageFailedEvent.Create(this.AggregateId, providerStatus, failedDateTime); + + this.ApplyAndPend(messageFailedEvent); + } + + /// + /// Marks the message as rejected. + /// + /// The provider status. + /// The rejected date time. + public void MarkMessageAsRejected(String providerStatus, + DateTime rejectedDateTime) + { + this.CheckMessageCanBeSetToRejected(); + + MessageRejectedEvent messageRejectedEvent = MessageRejectedEvent.Create(this.AggregateId, providerStatus, rejectedDateTime); + + this.ApplyAndPend(messageRejectedEvent); + } + + /// + /// Marks the message as spam. + /// + /// The provider status. + /// The spam date time. + public void MarkMessageAsSpam(String providerStatus, + DateTime spamDateTime) + { + this.CheckMessageCanBeSetToSpam(); + + MessageMarkedAsSpamEvent messageMarkedAsSpamEvent = MessageMarkedAsSpamEvent.Create(this.AggregateId, providerStatus, spamDateTime); + + this.ApplyAndPend(messageMarkedAsSpamEvent); + } + /// /// Messages the send to recipient failure. /// @@ -173,6 +257,66 @@ protected override void PlayEvent(DomainEvent domainEvent) this.PlayEvent((dynamic)domainEvent); } + /// + /// Checks the message can be set to bounced. + /// + /// Message at status {this.MessageStatus} cannot be set to bounced + private void CheckMessageCanBeSetToBounced() + { + if (this.MessageStatus != MessageStatus.Sent) + { + throw new InvalidOperationException($"Message at status {this.MessageStatus} cannot be set to bounced"); + } + } + + /// + /// Checks the message can be set to delivered. + /// + /// Message at status {this.MessageStatus} cannot be set to delivered + private void CheckMessageCanBeSetToDelivered() + { + if (this.MessageStatus != MessageStatus.Sent) + { + throw new InvalidOperationException($"Message at status {this.MessageStatus} cannot be set to delivered"); + } + } + + /// + /// Checks the message can be set to failed. + /// + /// Message at status {this.MessageStatus} cannot be set to failed + private void CheckMessageCanBeSetToFailed() + { + if (this.MessageStatus != MessageStatus.Sent) + { + throw new InvalidOperationException($"Message at status {this.MessageStatus} cannot be set to failed"); + } + } + + /// + /// Checks the message can be set to rejected. + /// + /// Message at status {this.MessageStatus} cannot be set to rejected + private void CheckMessageCanBeSetToRejected() + { + if (this.MessageStatus != MessageStatus.Sent) + { + throw new InvalidOperationException($"Message at status {this.MessageStatus} cannot be set to rejected"); + } + } + + /// + /// Checks the message can be set to spam. + /// + /// Message at status {this.MessageStatus} cannot be set to spam + private void CheckMessageCanBeSetToSpam() + { + if (this.MessageStatus != MessageStatus.Sent) + { + throw new InvalidOperationException($"Message at status {this.MessageStatus} cannot be set to spam"); + } + } + /// /// Plays the event. /// @@ -183,6 +327,7 @@ private void PlayEvent(RequestSentToProviderEvent domainEvent) this.Subject = domainEvent.Subject; this.IsHtml = domainEvent.IsHtml; this.FromAddress = domainEvent.FromAddress; + this.MessageStatus = MessageStatus.NotSet; foreach (String domainEventToAddress in domainEvent.ToAddresses) { @@ -200,41 +345,52 @@ private void PlayEvent(ResponseReceivedFromProviderEvent domainEvent) { this.ProviderEmailReference = domainEvent.ProviderEmailReference; this.ProviderRequestReference = domainEvent.ProviderRequestReference; + this.MessageStatus = MessageStatus.Sent; } - #endregion - } - - /// - /// - /// - internal class MessageRecipient - { - #region Constructors - - #endregion - - #region Properties + /// + /// Plays the event. + /// + /// The domain event. + private void PlayEvent(MessageDeliveredEvent domainEvent) + { + this.MessageStatus = MessageStatus.Delivered; + } /// - /// Converts to address. + /// Plays the event. /// - /// - /// To address. - /// - internal String ToAddress { get; private set; } + /// The domain event. + private void PlayEvent(MessageFailedEvent domainEvent) + { + this.MessageStatus = MessageStatus.Failed; + } - #endregion + /// + /// Plays the event. + /// + /// The domain event. + private void PlayEvent(MessageRejectedEvent domainEvent) + { + this.MessageStatus = MessageStatus.Rejected; + } - #region Methods + /// + /// Plays the event. + /// + /// The domain event. + private void PlayEvent(MessageBouncedEvent domainEvent) + { + this.MessageStatus = MessageStatus.Bounced; + } /// - /// Creates the specified to address. + /// Plays the event. /// - /// To address. - internal void Create(String toAddress) + /// The domain event. + private void PlayEvent(MessageMarkedAsSpamEvent domainEvent) { - this.ToAddress = toAddress; + this.MessageStatus = MessageStatus.Spam; } #endregion diff --git a/MessagingService.EmailMessageAggregate/MessageRecipient.cs b/MessagingService.EmailMessageAggregate/MessageRecipient.cs new file mode 100644 index 0000000..4bb0a98 --- /dev/null +++ b/MessagingService.EmailMessageAggregate/MessageRecipient.cs @@ -0,0 +1,39 @@ +namespace MessagingService.EmailMessageAggregate +{ + using System; + + /// + /// + /// + internal class MessageRecipient + { + #region Constructors + + #endregion + + #region Properties + + /// + /// Converts to address. + /// + /// + /// To address. + /// + internal String ToAddress { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified to address. + /// + /// To address. + internal void Create(String toAddress) + { + this.ToAddress = toAddress; + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.EmailMessageAggregate/MessageStatus.cs b/MessagingService.EmailMessageAggregate/MessageStatus.cs new file mode 100644 index 0000000..ef7a328 --- /dev/null +++ b/MessagingService.EmailMessageAggregate/MessageStatus.cs @@ -0,0 +1,43 @@ +namespace MessagingService.EmailMessageAggregate +{ + /// + /// + /// + public enum MessageStatus + { + /// + /// The not set + /// + NotSet = 0, + + /// + /// The sent + /// + Sent, + + /// + /// The delivered + /// + Delivered, + + /// + /// The bounced + /// + Bounced, + + /// + /// The rejected + /// + Rejected, + + /// + /// The failed + /// + Failed, + + /// + /// The spam + /// + Spam + } +} \ No newline at end of file diff --git a/MessagingService.Testing/TestData.cs b/MessagingService.Testing/TestData.cs index c858155..8816da5 100644 --- a/MessagingService.Testing/TestData.cs +++ b/MessagingService.Testing/TestData.cs @@ -5,8 +5,10 @@ using System.Collections.Generic; using System.Net; using BusinessLogic.Services.EmailServices; + using EmailMessage.DomainEvents; using EmailMessageAggregate; using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + using MessageStatus = BusinessLogic.Services.EmailServices.MessageStatus; public class TestData { @@ -38,6 +40,80 @@ public class TestData public static String ProviderEmailReference = "ProviderEmailReference"; + public static MessageStatus MessageStatusDelivered = MessageStatus.Delivered; + + public static MessageStatus MessageStatusRejected = MessageStatus.Rejected; + + public static MessageStatus MessageStatusFailed = MessageStatus.Failed; + + public static MessageStatus MessageStatusBounced = MessageStatus.Bounced; + + public static MessageStatus MessageStatusSpam = MessageStatus.Spam; + + public static MessageStatus MessageStatusUnknown = MessageStatus.Unknown; + + public static HttpStatusCode ProviderApiStatusCode = HttpStatusCode.OK; + + public static String ProviderStatusDescription = "delivered"; + + public static DateTime DeliveredDateTime = DateTime.Now; + + public static DateTime RejectedDateTime = DateTime.Now; + + public static DateTime BouncedDateTime = DateTime.Now; + + public static DateTime FailedDateTime = DateTime.Now; + + public static DateTime SpamDateTime = DateTime.Now; + + public static MessageStatusResponse MessageStatusResponseDelivered => + new MessageStatusResponse + { + ProviderStatusDescription = TestData.ProviderStatusDescription, + ApiStatusCode = TestData.ProviderApiStatusCode, + MessageStatus = TestData.MessageStatusDelivered + }; + + public static MessageStatusResponse MessageStatusResponseBounced => + new MessageStatusResponse + { + ProviderStatusDescription = TestData.ProviderStatusDescription, + ApiStatusCode = TestData.ProviderApiStatusCode, + MessageStatus = TestData.MessageStatusBounced + }; + + public static MessageStatusResponse MessageStatusResponseFailed => + new MessageStatusResponse + { + ProviderStatusDescription = TestData.ProviderStatusDescription, + ApiStatusCode = TestData.ProviderApiStatusCode, + MessageStatus = TestData.MessageStatusFailed + }; + + public static MessageStatusResponse MessageStatusResponseRejected => + new MessageStatusResponse + { + ProviderStatusDescription = TestData.ProviderStatusDescription, + ApiStatusCode = TestData.ProviderApiStatusCode, + MessageStatus = TestData.MessageStatusRejected + }; + + public static MessageStatusResponse MessageStatusResponseSpam => + new MessageStatusResponse + { + ProviderStatusDescription = TestData.ProviderStatusDescription, + ApiStatusCode = TestData.ProviderApiStatusCode, + MessageStatus = TestData.MessageStatusSpam + }; + + public static MessageStatusResponse MessageStatusResponseUnknown => + new MessageStatusResponse + { + ProviderStatusDescription = TestData.ProviderStatusDescription, + ApiStatusCode = TestData.ProviderApiStatusCode, + MessageStatus = TestData.MessageStatusUnknown + }; + public static EmailServiceProxyResponse SuccessfulEmailServiceProxyResponse => new EmailServiceProxyResponse { @@ -60,5 +136,17 @@ public static EmailAggregate GetEmptyEmailAggregate() return emailAggregate; } + + public static EmailAggregate GetSentEmailAggregate() + { + EmailAggregate emailAggregate = new EmailAggregate(); + emailAggregate.SendRequestToProvider(TestData.FromAddress, TestData.ToAddresses, TestData.Subject, + TestData.Body, TestData.IsHtmlTrue); + emailAggregate.ReceiveResponseFromProvider(TestData.ProviderRequestReference, TestData.ProviderEmailReference); + return emailAggregate; + } + + public static ResponseReceivedFromProviderEvent ResponseReceivedFromProviderEvent => + ResponseReceivedFromProviderEvent.Create(TestData.MessageId, TestData.ProviderRequestReference, TestData.ProviderEmailReference); } } \ No newline at end of file diff --git a/MessagingService/Controllers/DomainEventController.cs b/MessagingService/Controllers/DomainEventController.cs new file mode 100644 index 0000000..cb3e09f --- /dev/null +++ b/MessagingService/Controllers/DomainEventController.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MessagingService.Controllers +{ + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using BusinessLogic.EventHandling; + using Microsoft.AspNetCore.Mvc; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + using Shared.Logger; + + [Route(DomainEventController.ControllerRoute)] + [ApiController] + [ExcludeFromCodeCoverage] + public class DomainEventController : ControllerBase + { + private readonly IDomainEventHandlerResolver DomainEventHandlerResolver; + + public DomainEventController(IDomainEventHandlerResolver domainEventHandlerResolver) + { + this.DomainEventHandlerResolver = domainEventHandlerResolver; + } + + /// + /// Posts the event asynchronous. + /// + /// The domain event. + /// The cancellation token. + /// + [HttpPost] + public async Task PostEventAsync([FromBody] DomainEvent domainEvent, + CancellationToken cancellationToken) + { + cancellationToken.Register(() => this.Callback(cancellationToken, domainEvent.EventId)); + + try + { + Logger.LogInformation($"Processing event - ID [{domainEvent.EventId}], Type[{domainEvent.GetType().Name}]"); + + List eventHandlers = this.DomainEventHandlerResolver.GetDomainEventHandlers(domainEvent); + + if (eventHandlers == null || eventHandlers.Any() == false) + { + // Log a warning out + Logger.LogWarning($"No event handlers configured for Event Type [{domainEvent.GetType().Name}]"); + return this.Ok(); + } + + List tasks = new List(); + foreach (IDomainEventHandler domainEventHandler in eventHandlers) + { + tasks.Add(domainEventHandler.Handle(domainEvent, cancellationToken)); + } + + Task.WaitAll(tasks.ToArray()); + + Logger.LogInformation($"Finished processing event - ID [{domainEvent.EventId}]"); + + return this.Ok(); + } + catch (Exception ex) + { + String domainEventData = JsonConvert.SerializeObject(domainEvent); + Logger.LogError(new Exception($" Failed to Process Event, Event Data received [{domainEventData}]", ex)); + + throw; + } + } + + /// + /// Callbacks the specified cancellation token. + /// + /// The cancellation token. + /// The event identifier. + private void Callback(CancellationToken cancellationToken, + Guid eventId) + { + if (cancellationToken.IsCancellationRequested) //I think this would always be true anyway + { + Logger.LogInformation($"Cancel request for EventId {eventId}"); + cancellationToken.ThrowIfCancellationRequested(); + } + } + + #region Others + + /// + /// The controller name + /// + public const String ControllerName = "domainevents"; + + /// + /// The controller route + /// + private const String ControllerRoute = "api/" + DomainEventController.ControllerName; + + #endregion + + } +} diff --git a/MessagingService/Startup.cs b/MessagingService/Startup.cs index 737e465..edbac1d 100644 --- a/MessagingService/Startup.cs +++ b/MessagingService/Startup.cs @@ -17,6 +17,7 @@ namespace MessagingService using System.Net.Http; using System.Reflection; using BusinessLogic.Common; + using BusinessLogic.EventHandling; using BusinessLogic.RequestHandlers; using BusinessLogic.Requests; using BusinessLogic.Services; @@ -152,6 +153,28 @@ public void ConfigureServices(IServiceCollection services) return ConfigurationReader.GetBaseServerUri(serviceName).OriginalString; }); services.AddSingleton(); + + Dictionary eventHandlersConfiguration = new Dictionary(); + + if (Startup.Configuration != null) + { + IConfigurationSection section = Startup.Configuration.GetSection("AppSettings:EventHandlerConfiguration"); + + if (section != null) + { + Startup.Configuration.GetSection("AppSettings:EventHandlerConfiguration").Bind(eventHandlersConfiguration); + } + } + services.AddSingleton>(eventHandlersConfiguration); + + services.AddSingleton>(container => (type) => + { + IDomainEventHandler handler = container.GetService(type) as IDomainEventHandler; + return handler; + }); + + services.AddSingleton(); + services.AddSingleton(); } /// diff --git a/MessagingService/appsettings.json b/MessagingService/appsettings.json index 9f0a026..9e8230f 100644 --- a/MessagingService/appsettings.json +++ b/MessagingService/appsettings.json @@ -21,13 +21,18 @@ "AppSettings": { "UseConnectionStringConfig": false, "SecurityService": "http://192.168.1.133:5001", - "EmailProxy": "Smtp2Go", + "EmailProxy": "Smtp2Go", "SMTP2GoBaseAddress": "https://api.smtp2go.com/v3/", - "SMTP2GoAPIKey": "api-4CE2C6BC80D111EAB45BF23C91C88F4E" - }, - "SecurityConfiguration": { - "ApiName": "messagingService", - "Authority": "http://192.168.1.133:5001" + "SMTP2GoAPIKey": "api-4CE2C6BC80D111EAB45BF23C91C88F4E", + "EventHandlerConfiguration": { + "MessagingService.EmailMessage.DomainEvents.ResponseReceivedFromProviderEvent": [ + "MessagingService.BusinessLogic.EventHandling.EmailDomainEventHandler" + ] + } }, - "AllowedHosts": "*" -} + "SecurityConfiguration": { + "ApiName": "messagingService", + "Authority": "http://192.168.1.133:5001" + }, + "AllowedHosts": "*" + }