diff --git a/Sample/Helpdesk/Helpdesk.Api/Core/Kafka/KafkaProducer.cs b/Sample/Helpdesk/Helpdesk.Api/Core/Kafka/KafkaProducer.cs index d4c28773e..89047bab5 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Core/Kafka/KafkaProducer.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Core/Kafka/KafkaProducer.cs @@ -6,55 +6,4 @@ namespace Helpdesk.Api.Core.Kafka; -public class KafkaProducer: IProjection -{ - private const string DefaultConfigKey = "KafkaProducer"; - private readonly KafkaProducerConfig config; - - public KafkaProducer(IConfiguration configuration) - { - config = configuration.GetRequiredSection(DefaultConfigKey).Get() ?? - throw new InvalidOperationException(); - } - - public async Task ApplyAsync(IDocumentOperations operations, IReadOnlyList streamsActions, - CancellationToken ct) - { - foreach (var @event in streamsActions.SelectMany(streamAction => streamAction.Events)) - { - await Publish(@event.Data, ct); - } - } - - public void Apply(IDocumentOperations operations, IReadOnlyList streams) => - throw new NotImplementedException("Producer should be only used in the AsyncDaemon"); - - private async Task Publish(object @event, CancellationToken ct) - { - try - { - using var producer = new ProducerBuilder(config.ProducerConfig).Build(); - - await producer.ProduceAsync(config.Topic, - new Message - { - // store event type name in message Key - Key = @event.GetType().Name, - // serialize event to message Value - Value = JsonSerializer.Serialize(@event) - }, ct).ConfigureAwait(false); - } - catch (Exception exc) - { - Console.WriteLine(exc.Message); - throw; - } - } -} - -public class KafkaProducerConfig -{ - public ProducerConfig? ProducerConfig { get; set; } - public string? Topic { get; set; } -} diff --git a/Sample/Helpdesk/Helpdesk.Api/Core/Marten/DocumentSessionExtensions.cs b/Sample/Helpdesk/Helpdesk.Api/Core/Marten/DocumentSessionExtensions.cs index d62aa2674..5c673c436 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Core/Marten/DocumentSessionExtensions.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Core/Marten/DocumentSessionExtensions.cs @@ -2,22 +2,3 @@ namespace Helpdesk.Api.Core.Marten; -public static class DocumentSessionExtensions -{ - public static Task Add(this IDocumentSession documentSession, Guid id, object @event, CancellationToken ct) - where T : class - { - documentSession.Events.StartStream(id, @event); - return documentSession.SaveChangesAsync(token: ct); - } - - public static Task GetAndUpdate( - this IDocumentSession documentSession, - Guid id, - int version, - Func handle, - CancellationToken ct - ) where T : class => - documentSession.Events.WriteToAggregate(id, version, stream => - stream.AppendOne(handle(stream.Aggregate)), ct); -} diff --git a/Sample/Helpdesk/Helpdesk.Api/Core/SignalR/SignalRProducer.cs b/Sample/Helpdesk/Helpdesk.Api/Core/SignalR/SignalRProducer.cs index e1b869255..f203afbfb 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Core/SignalR/SignalRProducer.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Core/SignalR/SignalRProducer.cs @@ -5,23 +5,5 @@ namespace Helpdesk.Api.Core.SignalR; -public class SignalRProducer: IProjection -{ - private readonly IHubContext hubContext; - public SignalRProducer(IHubContext hubContext) => - this.hubContext = hubContext; - - public async Task ApplyAsync(IDocumentOperations operations, IReadOnlyList streamsActions, - CancellationToken ct) - { - foreach (var @event in streamsActions.SelectMany(streamAction => streamAction.Events)) - { - await hubContext.Clients.All.SendAsync(@event.EventTypeName, @event.Data, ct); - } - } - - public void Apply(IDocumentOperations operations, IReadOnlyList streams) => - throw new NotImplementedException("Producer should be only used in the AsyncDaemon"); -} diff --git a/Sample/Helpdesk/Helpdesk.Api/Helpdesk.Api.csproj b/Sample/Helpdesk/Helpdesk.Api/Helpdesk.Api.csproj index 3e9f5b7ca..e32c92767 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Helpdesk.Api.csproj +++ b/Sample/Helpdesk/Helpdesk.Api/Helpdesk.Api.csproj @@ -15,4 +15,10 @@ + + + + + + diff --git a/Sample/Helpdesk/Helpdesk.Api/Incidents/GetCustomerIncidentsSummary/IncidentSummary.cs b/Sample/Helpdesk/Helpdesk.Api/Incidents/GetCustomerIncidentsSummary/IncidentSummary.cs index 818cf2567..21808339b 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Incidents/GetCustomerIncidentsSummary/IncidentSummary.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Incidents/GetCustomerIncidentsSummary/IncidentSummary.cs @@ -5,75 +5,3 @@ namespace Helpdesk.Api.Incidents.GetCustomerIncidentsSummary; -public class CustomerIncidentsSummary -{ - public Guid Id { get; set; } - public int Pending { get; set; } - public int Resolved { get; set; } - public int Acknowledged { get; set; } - public int Closed { get; set; } -} - -public class CustomerIncidentsSummaryProjection: MultiStreamProjection -{ - public CustomerIncidentsSummaryProjection() - { - Identity(e => e.CustomerId); - CustomGrouping(new CustomerIncidentsSummaryGrouper()); - } - - public void Apply(IncidentLogged logged, CustomerIncidentsSummary current) - { - current.Pending++; - } - - public void Apply(IncidentResolved resolved, CustomerIncidentsSummary current) - { - current.Pending--; - current.Resolved++; - } - - public void Apply(ResolutionAcknowledgedByCustomer acknowledged, CustomerIncidentsSummary current) - { - current.Resolved--; - current.Acknowledged++; - } - - public void Apply(IncidentClosed closed, CustomerIncidentsSummary current) - { - current.Acknowledged--; - current.Closed++; - } -} - -public class CustomerIncidentsSummaryGrouper: IAggregateGrouper -{ - private readonly Type[] eventTypes = - { - typeof(IncidentResolved), typeof(ResolutionAcknowledgedByCustomer), - typeof(IncidentClosed) - }; - - public async Task Group(IQuerySession session, IEnumerable events, ITenantSliceGroup grouping) - { - var filteredEvents = events - .Where(ev => eventTypes.Contains(ev.EventType)) - .ToList(); - - if (!filteredEvents.Any()) - return; - - var incidentIds = filteredEvents.Select(e => e.StreamId).ToList(); - - var result = await session.Events.QueryRawEventDataOnly() - .Where(e => incidentIds.Contains(e.IncidentId)) - .Select(x => new { x.IncidentId, x.CustomerId }) - .ToListAsync(); - - foreach (var group in result.Select(g => - new { g.CustomerId, Events = filteredEvents.Where(ev => ev.StreamId == g.IncidentId) })) - { - grouping.AddEvents(group.CustomerId, group.Events); - } - } -} diff --git a/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentDetails/IncidentDetails.cs b/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentDetails/IncidentDetails.cs index 949cc4e2f..260a45557 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentDetails/IncidentDetails.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentDetails/IncidentDetails.cs @@ -2,80 +2,3 @@ namespace Helpdesk.Api.Incidents.GetIncidentDetails; -public record IncidentDetails( - Guid Id, - Guid CustomerId, - IncidentStatus Status, - IncidentNote[] Notes, - IncidentCategory? Category = null, - IncidentPriority? Priority = null, - Guid? AgentId = null, - int Version = 1 -); - -public record IncidentNote( - IncidentNoteType Type, - Guid From, - string Content, - bool VisibleToCustomer -); - -public enum IncidentNoteType -{ - FromAgent, - FromCustomer -} - -public class IncidentDetailsProjection: SingleStreamProjection -{ - public static IncidentDetails Create(IncidentLogged logged) => - new(logged.IncidentId, logged.CustomerId, IncidentStatus.Pending, Array.Empty()); - - public IncidentDetails Apply(IncidentCategorised categorised, IncidentDetails current) => - current with { Category = categorised.Category }; - - public IncidentDetails Apply(IncidentPrioritised prioritised, IncidentDetails current) => - current with { Priority = prioritised.Priority }; - - public IncidentDetails Apply(AgentAssignedToIncident prioritised, IncidentDetails current) => - current with { AgentId = prioritised.AgentId }; - - public IncidentDetails Apply(AgentRespondedToIncident agentResponded, IncidentDetails current) => - current with - { - Notes = current.Notes.Union( - new[] - { - new IncidentNote( - IncidentNoteType.FromAgent, - agentResponded.Response.AgentId, - agentResponded.Response.Content, - agentResponded.Response.VisibleToCustomer - ) - }).ToArray() - }; - - public IncidentDetails Apply(CustomerRespondedToIncident customerResponded, IncidentDetails current) => - current with - { - Notes = current.Notes.Union( - new[] - { - new IncidentNote( - IncidentNoteType.FromCustomer, - customerResponded.Response.CustomerId, - customerResponded.Response.Content, - true - ) - }).ToArray() - }; - - public IncidentDetails Apply(IncidentResolved resolved, IncidentDetails current) => - current with { Status = IncidentStatus.Resolved }; - - public IncidentDetails Apply(ResolutionAcknowledgedByCustomer acknowledged, IncidentDetails current) => - current with { Status = IncidentStatus.ResolutionAcknowledgedByCustomer }; - - public IncidentDetails Apply(IncidentClosed closed, IncidentDetails current) => - current with { Status = IncidentStatus.Closed }; -} diff --git a/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentHistory/IncidentHistory.cs b/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentHistory/IncidentHistory.cs index fbf243b53..736349339 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentHistory/IncidentHistory.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentHistory/IncidentHistory.cs @@ -3,113 +3,3 @@ using Marten.Schema.Identity; namespace Helpdesk.Api.Incidents.GetIncidentHistory; - -public record IncidentHistory( - Guid Id, - Guid IncidentId, - string Description -); - -public class IncidentHistoryTransformation: EventProjection -{ - public IncidentHistory Transform(IEvent input) - { - var (incidentId, customerId, contact, description, loggedBy, loggedAt) = input.Data; - - return new IncidentHistory( - CombGuidIdGeneration.NewGuid(), - incidentId, - $"['{loggedAt}'] Logged Incident with id: '{incidentId}' for customer '{customerId}' and description `{description}' through {contact} by '{loggedBy}'" - ); - } - - public IncidentHistory Transform(IEvent input) - { - var (incidentId, category, categorisedBy, categorisedAt) = input.Data; - - return new IncidentHistory( - CombGuidIdGeneration.NewGuid(), - incidentId, - $"[{categorisedAt}] Categorised Incident with id: '{incidentId}' as {category} by {categorisedBy}" - ); - } - - public IncidentHistory Transform(IEvent input) - { - var (incidentId, priority, prioritisedBy, prioritisedAt) = input.Data; - - return new IncidentHistory( - CombGuidIdGeneration.NewGuid(), - incidentId, - $"[{prioritisedAt}] Prioritised Incident with id: '{incidentId}' as '{priority}' by {prioritisedBy}" - ); - } - - public IncidentHistory Transform(IEvent input) - { - var (incidentId, agentId, assignedAt) = input.Data; - - return new IncidentHistory( - CombGuidIdGeneration.NewGuid(), - incidentId, - $"[{assignedAt}] Assigned agent `{agentId} to incident with id: '{incidentId}'" - ); - } - - public IncidentHistory Transform(IEvent input) - { - var (incidentId, response, respondedAt) = input.Data; - - return new IncidentHistory( - CombGuidIdGeneration.NewGuid(), - incidentId, - $"[{respondedAt}] Agent '{response.CustomerId}' responded with response '{response.Content}' to Incident with id: '{incidentId}'" - ); - } - - public IncidentHistory Transform(IEvent input) - { - var (incidentId, response, respondedAt) = input.Data; - - var responseVisibility = response.VisibleToCustomer ? "public" : "private"; - - return new IncidentHistory( - CombGuidIdGeneration.NewGuid(), - incidentId, - $"[{respondedAt}] Agent '{response.AgentId}' responded with {responseVisibility} response '{response.Content}' to Incident with id: '{incidentId}'" - ); - } - - public IncidentHistory Transform(IEvent input) - { - var (incidentId, resolution, resolvedBy, resolvedAt) = input.Data; - - return new IncidentHistory( - CombGuidIdGeneration.NewGuid(), - incidentId, - $"[{resolvedAt}] Resolved Incident with id: '{incidentId}' with resolution `{resolution} by '{resolvedBy}'" - ); - } - - public IncidentHistory Transform(IEvent input) - { - var (incidentId, acknowledgedBy, acknowledgedAt) = input.Data; - - return new IncidentHistory( - CombGuidIdGeneration.NewGuid(), - incidentId, - $"[{acknowledgedAt}] Customer '{acknowledgedBy}' acknowledged resolution of Incident with id: '{incidentId}'" - ); - } - - public IncidentHistory Transform(IEvent input) - { - var (incidentId, closedBy, closedAt) = input.Data; - - return new IncidentHistory( - CombGuidIdGeneration.NewGuid(), - incidentId, - $"[{closedAt}] Agent '{closedBy}' closed Incident with id: '{incidentId}'" - ); - } -} diff --git a/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentShortInfo/IncidentShortInfo.cs b/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentShortInfo/IncidentShortInfo.cs index d1a906359..3ae9990d6 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentShortInfo/IncidentShortInfo.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Incidents/GetIncidentShortInfo/IncidentShortInfo.cs @@ -1,39 +1,3 @@ using Marten.Events.Aggregation; namespace Helpdesk.Api.Incidents.GetIncidentShortInfo; - -public record IncidentShortInfo( - Guid Id, - Guid CustomerId, - IncidentStatus Status, - int NotesCount, - IncidentCategory? Category = null, - IncidentPriority? Priority = null -); - -public class IncidentShortInfoProjection: SingleStreamProjection -{ - public static IncidentShortInfo Create(IncidentLogged logged) => - new(logged.IncidentId, logged.CustomerId, IncidentStatus.Pending, 0); - - public IncidentShortInfo Apply(IncidentCategorised categorised, IncidentShortInfo current) => - current with { Category = categorised.Category }; - - public IncidentShortInfo Apply(IncidentPrioritised prioritised, IncidentShortInfo current) => - current with { Priority = prioritised.Priority }; - - public IncidentShortInfo Apply(AgentRespondedToIncident agentResponded, IncidentShortInfo current) => - current with { NotesCount = current.NotesCount + 1 }; - - public IncidentShortInfo Apply(CustomerRespondedToIncident customerResponded, IncidentShortInfo current) => - current with { NotesCount = current.NotesCount + 1 }; - - public IncidentShortInfo Apply(IncidentResolved resolved, IncidentShortInfo current) => - current with { Status = IncidentStatus.Resolved }; - - public IncidentShortInfo Apply(ResolutionAcknowledgedByCustomer acknowledged, IncidentShortInfo current) => - current with { Status = IncidentStatus.ResolutionAcknowledgedByCustomer }; - - public IncidentShortInfo Apply(IncidentClosed closed, IncidentShortInfo current) => - current with { Status = IncidentStatus.Closed }; -} diff --git a/Sample/Helpdesk/Helpdesk.Api/Incidents/Incident.cs b/Sample/Helpdesk/Helpdesk.Api/Incidents/Incident.cs index 38d2c0fce..cae324fc0 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Incidents/Incident.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Incidents/Incident.cs @@ -1,154 +1,3 @@ namespace Helpdesk.Api.Incidents; -public record IncidentLogged( - Guid IncidentId, - Guid CustomerId, - Contact Contact, - string Description, - Guid LoggedBy, - DateTimeOffset LoggedAt -); - -public record IncidentCategorised( - Guid IncidentId, - IncidentCategory Category, - Guid CategorisedBy, - DateTimeOffset CategorisedAt -); - -public record IncidentPrioritised( - Guid IncidentId, - IncidentPriority Priority, - Guid PrioritisedBy, - DateTimeOffset PrioritisedAt -); - -public record AgentAssignedToIncident( - Guid IncidentId, - Guid AgentId, - DateTimeOffset AssignedAt -); - -public record AgentRespondedToIncident( - Guid IncidentId, - IncidentResponse.FromAgent Response, - DateTimeOffset RespondedAt -); - -public record CustomerRespondedToIncident( - Guid IncidentId, - IncidentResponse.FromCustomer Response, - DateTimeOffset RespondedAt -); - -public record IncidentResolved( - Guid IncidentId, - ResolutionType Resolution, - Guid ResolvedBy, - DateTimeOffset ResolvedAt -); - -public record ResolutionAcknowledgedByCustomer( - Guid IncidentId, - Guid AcknowledgedBy, - DateTimeOffset AcknowledgedAt -); - -public record IncidentClosed( - Guid IncidentId, - Guid ClosedBy, - DateTimeOffset ClosedAt -); - -public enum IncidentStatus -{ - Pending = 1, - Resolved = 8, - ResolutionAcknowledgedByCustomer = 16, - Closed = 32 -} - -public record Incident( - Guid Id, - IncidentStatus Status, - bool HasOutstandingResponseToCustomer = false -) -{ - public static Incident Create(IncidentLogged logged) => - new(logged.IncidentId, IncidentStatus.Pending); - - public Incident Apply(AgentRespondedToIncident agentResponded) => - this with { HasOutstandingResponseToCustomer = false }; - - public Incident Apply(CustomerRespondedToIncident customerResponded) => - this with { HasOutstandingResponseToCustomer = true }; - - public Incident Apply(IncidentResolved resolved) => - this with { Status = IncidentStatus.Resolved }; - - public Incident Apply(ResolutionAcknowledgedByCustomer acknowledged) => - this with { Status = IncidentStatus.ResolutionAcknowledgedByCustomer }; - - public Incident Apply(IncidentClosed closed) => - this with { Status = IncidentStatus.Closed }; -} - -public enum IncidentCategory -{ - Software, - Hardware, - Network, - Database -} - -public enum IncidentPriority -{ - Critical, - High, - Medium, - Low -} - -public enum ResolutionType -{ - Temporary, - Permanent, - NotAnIncident -} - -public enum ContactChannel -{ - Email, - Phone, - InPerson, - GeneratedBySystem -} - -public record Contact( - ContactChannel ContactChannel, - string? FirstName = null, - string? LastName = null, - string? EmailAddress = null, - string? PhoneNumber = null -); - -public abstract record IncidentResponse -{ - public record FromAgent( - Guid AgentId, - string Content, - bool VisibleToCustomer - ): IncidentResponse(Content); - - public record FromCustomer( - Guid CustomerId, - string Content - ): IncidentResponse(Content); - - public string Content { get; init; } - - private IncidentResponse(string content) - { - Content = content; - } -} +// Hey! diff --git a/Sample/Helpdesk/Helpdesk.Api/Incidents/IncidentService.cs b/Sample/Helpdesk/Helpdesk.Api/Incidents/IncidentService.cs index c560ba759..17792f2a9 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Incidents/IncidentService.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Incidents/IncidentService.cs @@ -1,172 +1,2 @@ namespace Helpdesk.Api.Incidents; -public record LogIncident( - Guid IncidentId, - Guid CustomerId, - Contact Contact, - string Description, - Guid LoggedBy, - DateTimeOffset Now -); - -public record CategoriseIncident( - Guid IncidentId, - IncidentCategory Category, - Guid CategorisedBy, - DateTimeOffset Now -); - -public record PrioritiseIncident( - Guid IncidentId, - IncidentPriority Priority, - Guid PrioritisedBy, - DateTimeOffset Now -); - -public record AssignAgentToIncident( - Guid IncidentId, - Guid AgentId, - DateTimeOffset Now -); - -public record RecordAgentResponseToIncident( - Guid IncidentId, - IncidentResponse.FromAgent Response, - DateTimeOffset Now -); - -public record RecordCustomerResponseToIncident( - Guid IncidentId, - IncidentResponse.FromCustomer Response, - DateTimeOffset Now -); - -public record ResolveIncident( - Guid IncidentId, - ResolutionType Resolution, - Guid ResolvedBy, - DateTimeOffset Now -); - -public record AcknowledgeResolution( - Guid IncidentId, - Guid AcknowledgedBy, - DateTimeOffset Now -); - -public record CloseIncident( - Guid IncidentId, - Guid ClosedBy, - DateTimeOffset Now -); - -internal static class IncidentService -{ - public static IncidentLogged Handle(LogIncident command) - { - var (incidentId, customerId, contact, description, loggedBy, now) = command; - - return new IncidentLogged(incidentId, customerId, contact, description, loggedBy, now); - } - - public static IncidentCategorised Handle(Incident current, CategoriseIncident command) - { - if (current.Status == IncidentStatus.Closed) - throw new InvalidOperationException("Incident is already closed"); - - var (incidentId, incidentCategory, categorisedBy, now) = command; - - return new IncidentCategorised(incidentId, incidentCategory, categorisedBy, now); - } - - public static IncidentPrioritised Handle(Incident current, PrioritiseIncident command) - { - if (current.Status == IncidentStatus.Closed) - throw new InvalidOperationException("Incident is already closed"); - - var (incidentId, incidentPriority, prioritisedBy, now) = command; - - return new IncidentPrioritised(incidentId, incidentPriority, prioritisedBy, now); - } - - public static AgentAssignedToIncident Handle(Incident current, AssignAgentToIncident command) - { - if (current.Status == IncidentStatus.Closed) - throw new InvalidOperationException("Incident is already closed"); - - var (incidentId, agentId, now) = command; - - return new AgentAssignedToIncident(incidentId, agentId, now); - } - - public static AgentRespondedToIncident Handle( - Incident current, - RecordAgentResponseToIncident command - ) - { - if (current.Status == IncidentStatus.Closed) - throw new InvalidOperationException("Incident is already closed"); - - var (incidentId, response, now) = command; - - return new AgentRespondedToIncident(incidentId, response, now); - } - - public static CustomerRespondedToIncident Handle( - Incident current, - RecordCustomerResponseToIncident command - ) - { - if (current.Status == IncidentStatus.Closed) - throw new InvalidOperationException("Incident is already closed"); - - var (incidentId, response, now) = command; - - return new CustomerRespondedToIncident(incidentId, response, now); - } - - public static IncidentResolved Handle( - Incident current, - ResolveIncident command - ) - { - if (current.Status is IncidentStatus.Resolved or IncidentStatus.Closed) - throw new InvalidOperationException("Cannot resolve already resolved or closed incident"); - - if (current.HasOutstandingResponseToCustomer) - throw new InvalidOperationException("Cannot resolve incident that has outstanding responses to customer"); - - var (incidentId, resolution, resolvedBy, now) = command; - - return new IncidentResolved(incidentId, resolution, resolvedBy, now); - } - - public static ResolutionAcknowledgedByCustomer Handle( - Incident current, - AcknowledgeResolution command - ) - { - if (current.Status is not IncidentStatus.Resolved) - throw new InvalidOperationException("Only resolved incident can be acknowledged"); - - var (incidentId, acknowledgedBy, now) = command; - - return new ResolutionAcknowledgedByCustomer(incidentId, acknowledgedBy, now); - } - - public static IncidentClosed Handle( - Incident current, - CloseIncident command - ) - { - if (current.Status is not IncidentStatus.ResolutionAcknowledgedByCustomer) - throw new InvalidOperationException("Only incident with acknowledged resolution can be closed"); - - if (current.HasOutstandingResponseToCustomer) - throw new InvalidOperationException("Cannot close incident that has outstanding responses to customer"); - - var (incidentId, acknowledgedBy, now) = command; - - return new IncidentClosed(incidentId, acknowledgedBy, now); - } -} diff --git a/Sample/Helpdesk/Helpdesk.Api/Program.cs b/Sample/Helpdesk/Helpdesk.Api/Program.cs index 736d2535a..48a24dc09 100644 --- a/Sample/Helpdesk/Helpdesk.Api/Program.cs +++ b/Sample/Helpdesk/Helpdesk.Api/Program.cs @@ -1,27 +1,4 @@ -using System.Text.Json.Serialization; -using Helpdesk.Api.Core.Http; -using Helpdesk.Api.Core.Kafka; -using Helpdesk.Api.Core.Marten; -using Helpdesk.Api.Core.SignalR; -using Helpdesk.Api.Incidents; -using Helpdesk.Api.Incidents.GetCustomerIncidentsSummary; -using Helpdesk.Api.Incidents.GetIncidentDetails; -using Helpdesk.Api.Incidents.GetIncidentHistory; -using Helpdesk.Api.Incidents.GetIncidentShortInfo; -using JasperFx.CodeGeneration; -using Marten; -using Marten.AspNetCore; -using Marten.Events.Daemon.Resiliency; -using Marten.Events.Projections; -using Marten.Pagination; -using Marten.Schema.Identity; -using Marten.Services.Json; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; -using Oakton; -using Weasel.Core; using static Microsoft.AspNetCore.Http.TypedResults; -using static Helpdesk.Api.Incidents.IncidentService; using static Helpdesk.Api.Core.Http.ETagExtensions; using static System.DateTimeOffset; using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; @@ -31,255 +8,9 @@ builder.Services .AddEndpointsApiExplorer() - .AddSwaggerGen() - .AddMarten(sp => - { - var options = new StoreOptions(); + .AddSwaggerGen(); - var schemaName = Environment.GetEnvironmentVariable("SchemaName") ?? "Helpdesk"; - options.Events.DatabaseSchemaName = schemaName; - options.DatabaseSchemaName = schemaName; - options.Connection(builder.Configuration.GetConnectionString("Incidents") ?? - throw new InvalidOperationException()); - - options.UseDefaultSerialization( - EnumStorage.AsString, - nonPublicMembersStorage: NonPublicMembersStorage.All, - serializerType: SerializerType.SystemTextJson - ); - - options.Projections.LiveStreamAggregation(); - options.Projections.Add(ProjectionLifecycle.Inline); - options.Projections.Add(ProjectionLifecycle.Inline); - options.Projections.Add(ProjectionLifecycle.Inline); - options.Projections.Add(ProjectionLifecycle.Async); - options.Projections.Add(new KafkaProducer(builder.Configuration), ProjectionLifecycle.Async); - options.Projections.Add( - new SignalRProducer((IHubContext)sp.GetRequiredService>()), - ProjectionLifecycle.Async - ); - - return options; - }) - .OptimizeArtifactWorkflow(TypeLoadMode.Static) - .UseLightweightSessions() - .AddAsyncDaemon(DaemonMode.Solo); - -builder.Services - .AddCors(options => - options.AddPolicy("ClientPermission", policy => - policy.WithOrigins("http://localhost:3000") - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials() - ) - ) - .Configure(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter())) - .Configure(o => - o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())) - .AddSignalR(); - -builder.Host.ApplyOaktonExtensions(); var app = builder.Build(); -var customersIncidents = app.MapGroup("api/customers/{customerId:guid}/incidents/").WithTags("Customer", "Incident"); -var agentIncidents = app.MapGroup("api/agents/{agentId:guid}/incidents/").WithTags("Agent", "Incident"); -var incidents = app.MapGroup("api/incidents").WithTags("Incident"); - -customersIncidents.MapPost("", - async ( - IDocumentSession documentSession, - Guid customerId, - LogIncidentRequest body, - CancellationToken ct) => - { - var (contact, description) = body; - var incidentId = CombGuidIdGeneration.NewGuid(); - - await documentSession.Add(incidentId, - Handle(new LogIncident(incidentId, customerId, contact, description, customerId, Now)), ct); - - return Created($"/api/incidents/{incidentId}", incidentId); - } -); - -agentIncidents.MapPost("{incidentId:guid}/category", - ( - IDocumentSession documentSession, - Guid incidentId, - Guid agentId, - [FromIfMatchHeader] string eTag, - CategoriseIncidentRequest body, - CancellationToken ct - ) => - documentSession.GetAndUpdate(incidentId, ToExpectedVersion(eTag), - state => Handle(state, new CategoriseIncident(incidentId, body.Category, agentId, Now)), ct) -); - -agentIncidents.MapPost("{incidentId:guid}/priority", - ( - IDocumentSession documentSession, - Guid incidentId, - Guid agentId, - [FromIfMatchHeader] string eTag, - PrioritiseIncidentRequest body, - CancellationToken ct - ) => - documentSession.GetAndUpdate(incidentId, ToExpectedVersion(eTag), - state => Handle(state, new PrioritiseIncident(incidentId, body.Priority, agentId, Now)), ct) -); - -agentIncidents.MapPost("{incidentId:guid}/assign", - ( - IDocumentSession documentSession, - Guid incidentId, - Guid agentId, - [FromIfMatchHeader] string eTag, - CancellationToken ct - ) => - documentSession.GetAndUpdate(incidentId, ToExpectedVersion(eTag), - state => Handle(state, new AssignAgentToIncident(incidentId, agentId, Now)), ct) -); - -customersIncidents.MapPost("{incidentId:guid}/responses/", - ( - IDocumentSession documentSession, - Guid incidentId, - Guid customerId, - [FromIfMatchHeader] string eTag, - RecordCustomerResponseToIncidentRequest body, - CancellationToken ct - ) => - documentSession.GetAndUpdate(incidentId, ToExpectedVersion(eTag), - state => Handle(state, - new RecordCustomerResponseToIncident(incidentId, - new IncidentResponse.FromCustomer(customerId, body.Content), Now)), ct) -); - -agentIncidents.MapPost("{incidentId:guid}/responses/", - ( - IDocumentSession documentSession, - [FromIfMatchHeader] string eTag, - Guid incidentId, - Guid agentId, - RecordAgentResponseToIncidentRequest body, - CancellationToken ct - ) => - { - var (content, visibleToCustomer) = body; - - return documentSession.GetAndUpdate(incidentId, ToExpectedVersion(eTag), - state => Handle(state, - new RecordAgentResponseToIncident(incidentId, - new IncidentResponse.FromAgent(agentId, content, visibleToCustomer), Now)), ct); - } -); - -agentIncidents.MapPost("{incidentId:guid}/resolve", - ( - IDocumentSession documentSession, - Guid incidentId, - Guid agentId, - [FromIfMatchHeader] string eTag, - ResolveIncidentRequest body, - CancellationToken ct - ) => - documentSession.GetAndUpdate(incidentId, ToExpectedVersion(eTag), - state => Handle(state, new ResolveIncident(incidentId, body.Resolution, agentId, Now)), ct) -); - -customersIncidents.MapPost("{incidentId:guid}/acknowledge", - ( - IDocumentSession documentSession, - Guid incidentId, - Guid customerId, - [FromIfMatchHeader] string eTag, - CancellationToken ct - ) => - documentSession.GetAndUpdate(incidentId, ToExpectedVersion(eTag), - state => Handle(state, new AcknowledgeResolution(incidentId, customerId, Now)), ct) -); - -agentIncidents.MapPost("{incidentId:guid}/close", - async ( - IDocumentSession documentSession, - Guid incidentId, - Guid agentId, - [FromIfMatchHeader] string eTag, - CancellationToken ct) => - { - await documentSession.GetAndUpdate(incidentId, ToExpectedVersion(eTag), - state => Handle(state, new CloseIncident(incidentId, agentId, Now)), ct); - - return Ok(); - } -); - -customersIncidents.MapGet("", - (IQuerySession querySession, Guid customerId, [FromQuery] int? pageNumber, [FromQuery] int? pageSize, - CancellationToken ct) => - querySession.Query().Where(i => i.CustomerId == customerId) - .ToPagedListAsync(pageNumber ?? 1, pageSize ?? 10, ct) -); - -incidents.MapGet("{incidentId:guid}", - (HttpContext context, IQuerySession querySession, Guid incidentId) => - querySession.Json.WriteById(incidentId, context) -); - -incidents.MapGet("{incidentId:guid}/history", - (HttpContext context, IQuerySession querySession, Guid incidentId) => - querySession.Query().Where(i => i.IncidentId == incidentId).WriteArray(context) -); - -customersIncidents.MapGet("incidents-summary", - (HttpContext context, IQuerySession querySession, Guid customerId) => - querySession.Json.WriteById(customerId, context) -); - -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger() - .UseSwaggerUI(); -} - - -app.UseCors("ClientPermission"); -app.MapHub("/hubs/incidents"); - -return await app.RunOaktonCommands(args); - -public class IncidentsHub: Hub -{ -} - -public record LogIncidentRequest( - Contact Contact, - string Description -); - -public record CategoriseIncidentRequest( - IncidentCategory Category -); - -public record PrioritiseIncidentRequest( - IncidentPriority Priority -); - -public record RecordCustomerResponseToIncidentRequest( - string Content -); - -public record RecordAgentResponseToIncidentRequest( - string Content, - bool VisibleToCustomer -); - -public record ResolveIncidentRequest( - ResolutionType Resolution -); - -public partial class Program -{ -} +app.Run();