diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..37f28c3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/.github +**/.idea* +**/artifacts +LICENSE +README.md diff --git a/Expressions.sln b/Expressions.sln index 6e67915..4996c2d 100644 --- a/Expressions.sln +++ b/Expressions.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{7721DE8D .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props version.json = version.json + docker-compose.yml = docker-compose.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4695B427-BD61-4FA2-A3F4-995AC92C9B02}" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a9cb7aa --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: "3" +services: + helpdesk-relational-app: + image: raiqub.samples.helpdesk-relational + build: + context: . + dockerfile: samples/Helpdesk.Relational/Dockerfile + ports: + - "8080:80" + environment: + ASPNETCORE_ENVIRONMENT: Development + depends_on: + - helpdesk-relational-db + - helpdesk-relational-pgadmin + + helpdesk-relational-db: + image: clkao/postgres-plv8 + environment: + POSTGRES_PASSWORD: Password12! + + helpdesk-relational-pgadmin: + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + PGADMIN_CONFIG_SERVER_MODE: 'False' + volumes: + - pgadmin:/var/lib/pgadmin + ports: + - "${PGADMIN_PORT:-5050}:80" + depends_on: + - helpdesk-relational-db + +volumes: + pgadmin: diff --git a/samples/Helpdesk.Relational/Dockerfile b/samples/Helpdesk.Relational/Dockerfile new file mode 100644 index 0000000..48f0185 --- /dev/null +++ b/samples/Helpdesk.Relational/Dockerfile @@ -0,0 +1,19 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY . . +WORKDIR "/src/samples/Helpdesk.Relational" +RUN dotnet restore +RUN dotnet build "Helpdesk.Relational.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Helpdesk.Relational.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Raiqub.Helpdesk.Relational.dll"] diff --git a/samples/Helpdesk.Relational/Helpdesk.Relational.csproj b/samples/Helpdesk.Relational/Helpdesk.Relational.csproj index 221571e..7038f66 100644 --- a/samples/Helpdesk.Relational/Helpdesk.Relational.csproj +++ b/samples/Helpdesk.Relational/Helpdesk.Relational.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -6,7 +6,14 @@ - + + + + + + + + diff --git a/samples/Helpdesk.Relational/HelpdeskDbContext.cs b/samples/Helpdesk.Relational/HelpdeskDbContext.cs new file mode 100644 index 0000000..b7e2e9c --- /dev/null +++ b/samples/Helpdesk.Relational/HelpdeskDbContext.cs @@ -0,0 +1,44 @@ +using Helpdesk.Relational.Incidents; +using Helpdesk.Relational.Incidents.Categorise; +using Helpdesk.Relational.Incidents.LogNew; +using Helpdesk.Relational.Incidents.Prioritise; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Helpdesk.Relational; + +public class HelpdeskDbContext : DbContext +{ + public HelpdeskDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var incidentBuilder = modelBuilder.Entity(); + + incidentBuilder.Property(i => i.Id); + incidentBuilder.Property(i => i.CustomerId); + incidentBuilder.Property(i => i.Description); + incidentBuilder.Property(i => i.LoggedBy); + incidentBuilder.Property(i => i.LoggedAt); + incidentBuilder.Property(i => i.Status).HasConversion>(); + incidentBuilder.Property(i => i.Category).HasConversion>(); + incidentBuilder.Property(i => i.Priority).HasConversion>(); + incidentBuilder.HasKey(i => i.Id); + + incidentBuilder.OwnsOne(i => i.Contact) + .Property(c => c.ContactChannel).HasConversion>(); + incidentBuilder.HasMany(i => i.Responses).WithOne(); + + incidentBuilder.Navigation(i => i.Responses).AutoInclude(); + + var responseBuilder = modelBuilder.Entity(); + responseBuilder.Property("Id"); + responseBuilder.HasKey("Id"); + + responseBuilder.HasDiscriminator("type") + .HasValue("agent") + .HasValue("customer"); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/Acknowledge/AcknowledgeHandler.cs b/samples/Helpdesk.Relational/Incidents/Acknowledge/AcknowledgeScenario.cs similarity index 71% rename from samples/Helpdesk.Relational/Incidents/Acknowledge/AcknowledgeHandler.cs rename to samples/Helpdesk.Relational/Incidents/Acknowledge/AcknowledgeScenario.cs index 244405d..6624aff 100644 --- a/samples/Helpdesk.Relational/Incidents/Acknowledge/AcknowledgeHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/Acknowledge/AcknowledgeScenario.cs @@ -1,8 +1,8 @@ namespace Helpdesk.Relational.Incidents.Acknowledge; -public static class AcknowledgeHandler +public static class AcknowledgeScenario { - public static ResolutionAcknowledgedByCustomer Handle(Incident current, AcknowledgeResolution command) + public static ResolutionAcknowledgedByCustomer Execute(Incident current, AcknowledgeResolution command) { if (current.IsSatisfiedBy(IncidentSpecification.IsNotResolved)) throw new IncidentIsNotResolvedException(command.IncidentId); diff --git a/samples/Helpdesk.Relational/Incidents/Acknowledge/v1/AcknowledgeIncidentResolutionUseCase.cs b/samples/Helpdesk.Relational/Incidents/Acknowledge/v1/AcknowledgeIncidentResolutionUseCase.cs new file mode 100644 index 0000000..cd3cf5a --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/Acknowledge/v1/AcknowledgeIncidentResolutionUseCase.cs @@ -0,0 +1,22 @@ +using Raiqub.Expressions.Sessions; + +namespace Helpdesk.Relational.Incidents.Acknowledge.v1; + +public class AcknowledgeIncidentResolutionUseCase +{ + private readonly IDbSession _dbSession; + + public AcknowledgeIncidentResolutionUseCase(IDbSession dbSession) + { + _dbSession = dbSession; + } + + public async Task Execute(Guid incidentId, Guid customerId, CancellationToken cancellationToken) + { + var incident = await _dbSession.Query(IncidentSpecification.HasId(incidentId)).FirstAsync(cancellationToken); + incident.Apply(AcknowledgeScenario.Execute(incident, new AcknowledgeResolution(incidentId, customerId))); + + _dbSession.Update(incident); + await _dbSession.SaveChangesAsync(cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/Api/v1/GetAllOfCustomer/GetAllOfCustomerIncidentHandler.cs b/samples/Helpdesk.Relational/Incidents/Api/v1/GetAllOfCustomer/GetAllOfCustomerIncidentHandler.cs deleted file mode 100644 index ae4a6df..0000000 --- a/samples/Helpdesk.Relational/Incidents/Api/v1/GetAllOfCustomer/GetAllOfCustomerIncidentHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Helpdesk.Relational.Incidents.GetShortInfo; -using Raiqub.Expressions.Sessions; - -namespace Helpdesk.Relational.Incidents.Api.v1.GetAllOfCustomer; - -public class GetAllOfCustomerIncidentHandler -{ - private readonly IDbQuerySessionFactory _sessionFactory; - - public GetAllOfCustomerIncidentHandler(IDbQuerySessionFactory sessionFactory) - { - _sessionFactory = sessionFactory; - } - - public async Task> Execute( - GetAllOfCustomerIncidentRequest request, - CancellationToken cancellationToken) - { - await using var session = _sessionFactory.Create(); - - var incidentShortInfos = await session - .Query( - new GetIncidentShortInfoQueryModel(request.CustomerId, request.PageNumber ?? 1, request.PageSize ?? 10)) - .ToListAsync(cancellationToken); - - return incidentShortInfos; - } -} diff --git a/samples/Helpdesk.Relational/Incidents/Api/v1/GetAllOfCustomer/GetAllOfCustomerIncidentRequest.cs b/samples/Helpdesk.Relational/Incidents/Api/v1/GetAllOfCustomer/GetAllOfCustomerIncidentRequest.cs deleted file mode 100644 index 3299d67..0000000 --- a/samples/Helpdesk.Relational/Incidents/Api/v1/GetAllOfCustomer/GetAllOfCustomerIncidentRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Helpdesk.Relational.Incidents.Api.v1.GetAllOfCustomer; - -public record GetAllOfCustomerIncidentRequest( - Guid CustomerId, - int? PageNumber, - int? PageSize); diff --git a/samples/Helpdesk.Relational/Incidents/AssignAgent/AssignAgentHandler.cs b/samples/Helpdesk.Relational/Incidents/AssignAgent/AssignAgentScenario.cs similarity index 74% rename from samples/Helpdesk.Relational/Incidents/AssignAgent/AssignAgentHandler.cs rename to samples/Helpdesk.Relational/Incidents/AssignAgent/AssignAgentScenario.cs index f4eac86..84938b6 100644 --- a/samples/Helpdesk.Relational/Incidents/AssignAgent/AssignAgentHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/AssignAgent/AssignAgentScenario.cs @@ -2,9 +2,9 @@ namespace Helpdesk.Relational.Incidents.AssignAgent; -public static class AssignAgentHandler +public static class AssignAgentScenario { - public static AgentAssignedToIncident Handle(Incident current, AssignAgentToIncident command) + public static AgentAssignedToIncident Execute(Incident current, AssignAgentToIncident command) { if (current.IsSatisfiedBy(IncidentSpecification.IsClosed)) throw new IncidentAlreadyClosedException(command.IncidentId); diff --git a/samples/Helpdesk.Relational/Incidents/AssignAgent/v1/AssignAgentToIncidentUseCase.cs b/samples/Helpdesk.Relational/Incidents/AssignAgent/v1/AssignAgentToIncidentUseCase.cs new file mode 100644 index 0000000..71c06c7 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/AssignAgent/v1/AssignAgentToIncidentUseCase.cs @@ -0,0 +1,22 @@ +using Raiqub.Expressions.Sessions; + +namespace Helpdesk.Relational.Incidents.AssignAgent.v1; + +public class AssignAgentToIncidentUseCase +{ + private readonly IDbSession _dbSession; + + public AssignAgentToIncidentUseCase(IDbSession dbSession) + { + _dbSession = dbSession; + } + + public async Task Execute(Guid incidentId, Guid agentId, CancellationToken cancellationToken) + { + var incident = await _dbSession.Query(IncidentSpecification.HasId(incidentId)).FirstAsync(cancellationToken); + incident.Apply(AssignAgentScenario.Execute(incident, new AssignAgentToIncident(incidentId, agentId))); + + _dbSession.Update(incident); + await _dbSession.SaveChangesAsync(cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/Categorise/CategoriseHandler.cs b/samples/Helpdesk.Relational/Incidents/Categorise/CategoriseScenario.cs similarity index 76% rename from samples/Helpdesk.Relational/Incidents/Categorise/CategoriseHandler.cs rename to samples/Helpdesk.Relational/Incidents/Categorise/CategoriseScenario.cs index c79b413..a0b027b 100644 --- a/samples/Helpdesk.Relational/Incidents/Categorise/CategoriseHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/Categorise/CategoriseScenario.cs @@ -2,9 +2,9 @@ namespace Helpdesk.Relational.Incidents.Categorise; -public static class CategoriseHandler +public static class CategoriseScenario { - public static IncidentCategorised Handle(Incident current, CategoriseIncident command) + public static IncidentCategorised Execute(Incident current, CategoriseIncident command) { if (current.IsSatisfiedBy(IncidentSpecification.IsClosed)) throw new IncidentAlreadyClosedException(command.IncidentId); diff --git a/samples/Helpdesk.Relational/Incidents/Categorise/v1/CategoriseIncidentRequest.cs b/samples/Helpdesk.Relational/Incidents/Categorise/v1/CategoriseIncidentRequest.cs new file mode 100644 index 0000000..40834e2 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/Categorise/v1/CategoriseIncidentRequest.cs @@ -0,0 +1,3 @@ +namespace Helpdesk.Relational.Incidents.Categorise.v1; + +public record CategoriseIncidentRequest(IncidentCategory Category); diff --git a/samples/Helpdesk.Relational/Incidents/Categorise/v1/CategoriseIncidentUseCase.cs b/samples/Helpdesk.Relational/Incidents/Categorise/v1/CategoriseIncidentUseCase.cs new file mode 100644 index 0000000..f3565b8 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/Categorise/v1/CategoriseIncidentUseCase.cs @@ -0,0 +1,27 @@ +using Raiqub.Expressions.Sessions; + +namespace Helpdesk.Relational.Incidents.Categorise.v1; + +public class CategoriseIncidentUseCase +{ + private readonly IDbSession _dbSession; + + public CategoriseIncidentUseCase(IDbSession dbSession) + { + _dbSession = dbSession; + } + + public async Task Execute( + Guid incidentId, + Guid agentId, + CategoriseIncidentRequest request, + CancellationToken cancellationToken) + { + var incident = await _dbSession.Query(IncidentSpecification.HasId(incidentId)).FirstAsync(cancellationToken); + incident.Apply( + CategoriseScenario.Execute(incident, new CategoriseIncident(incidentId, request.Category, agentId))); + + _dbSession.Update(incident); + await _dbSession.SaveChangesAsync(cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/Close/CloseHandler.cs b/samples/Helpdesk.Relational/Incidents/Close/CloseScenario.cs similarity index 80% rename from samples/Helpdesk.Relational/Incidents/Close/CloseHandler.cs rename to samples/Helpdesk.Relational/Incidents/Close/CloseScenario.cs index b2f1a63..37c06c8 100644 --- a/samples/Helpdesk.Relational/Incidents/Close/CloseHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/Close/CloseScenario.cs @@ -1,8 +1,8 @@ namespace Helpdesk.Relational.Incidents.Close; -public static class CloseHandler +public static class CloseScenario { - public static IncidentClosed Handle(Incident current, CloseIncident command) + public static IncidentClosed Execute(Incident current, CloseIncident command) { if (current.Status is not IncidentStatus.ResolutionAcknowledgedByCustomer) throw new IncidentIsNotAcknowledgedException(command.IncidentId); diff --git a/samples/Helpdesk.Relational/Incidents/Close/v1/CloseIncidentUseCase.cs b/samples/Helpdesk.Relational/Incidents/Close/v1/CloseIncidentUseCase.cs new file mode 100644 index 0000000..dd2c682 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/Close/v1/CloseIncidentUseCase.cs @@ -0,0 +1,22 @@ +using Raiqub.Expressions.Sessions; + +namespace Helpdesk.Relational.Incidents.Close.v1; + +public class CloseIncidentUseCase +{ + private readonly IDbSession _dbSession; + + public CloseIncidentUseCase(IDbSession dbSession) + { + _dbSession = dbSession; + } + + public async Task Execute(Guid incidentId, Guid agentId, CancellationToken cancellationToken) + { + var incident = await _dbSession.Query(IncidentSpecification.HasId(incidentId)).FirstAsync(cancellationToken); + incident.Apply(CloseScenario.Execute(incident, new CloseIncident(incidentId, agentId))); + + _dbSession.Update(incident); + await _dbSession.SaveChangesAsync(cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/GetCustomerSummary/CustomerIncidentsSummary.cs b/samples/Helpdesk.Relational/Incidents/GetCustomerSummary/CustomerIncidentsSummary.cs new file mode 100644 index 0000000..676a8e0 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/GetCustomerSummary/CustomerIncidentsSummary.cs @@ -0,0 +1,9 @@ +namespace Helpdesk.Relational.Incidents.GetCustomerSummary; + +public record CustomerIncidentsSummary(Guid Id) +{ + public int Pending { get; init; } + public int Resolved { get; init; } + public int Acknowledged { get; init; } + public int Closed { get; init; } +} diff --git a/samples/Helpdesk.Relational/Incidents/GetCustomerSummary/GetCustomerIncidentsSummaryQueryModel.cs b/samples/Helpdesk.Relational/Incidents/GetCustomerSummary/GetCustomerIncidentsSummaryQueryModel.cs new file mode 100644 index 0000000..b855f39 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/GetCustomerSummary/GetCustomerIncidentsSummaryQueryModel.cs @@ -0,0 +1,19 @@ +using Raiqub.Expressions.Queries; + +namespace Helpdesk.Relational.Incidents.GetCustomerSummary; + +public class GetCustomerIncidentsSummaryQueryModel : IQueryModel<(IncidentStatus Key, int Count)> +{ + public GetCustomerIncidentsSummaryQueryModel(Guid customerId) => CustomerId = customerId; + + public Guid CustomerId { get; } + + public IQueryable<(IncidentStatus Key, int Count)> Execute(IQuerySource source) + { + return from pending in source.GetSet() + where pending.CustomerId == CustomerId + group pending by pending.Status + into g + select new ValueTuple(g.Key, g.Count()); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/GetCustomerSummary/v1/GetCustomerIncidentsSummaryUseCase.cs b/samples/Helpdesk.Relational/Incidents/GetCustomerSummary/v1/GetCustomerIncidentsSummaryUseCase.cs new file mode 100644 index 0000000..d1f85f3 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/GetCustomerSummary/v1/GetCustomerIncidentsSummaryUseCase.cs @@ -0,0 +1,31 @@ +using Raiqub.Expressions.Sessions; + +namespace Helpdesk.Relational.Incidents.GetCustomerSummary.v1; + +public class GetCustomerIncidentsSummaryUseCase +{ + private readonly IDbQuerySession _dbQuerySession; + + public GetCustomerIncidentsSummaryUseCase(IDbQuerySession dbQuerySession) + { + _dbQuerySession = dbQuerySession; + } + + public async Task Execute(Guid customerId, CancellationToken cancellationToken) + { + return await _dbQuerySession + .Query(new GetCustomerIncidentsSummaryQueryModel(customerId)) + .ToAsyncEnumerable(cancellationToken) + .AggregateAsync( + new CustomerIncidentsSummary(customerId), + (seed, acc) => acc.Key switch + { + IncidentStatus.Pending => seed with { Pending = acc.Count }, + IncidentStatus.Resolved => seed with { Resolved = acc.Count }, + IncidentStatus.ResolutionAcknowledgedByCustomer => seed with { Acknowledged = acc.Count }, + IncidentStatus.Closed => seed with { Closed = acc.Count }, + _ => throw new InvalidOperationException() + }, + cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/GetDetails/GetIncidentDetailsQueryModel.cs b/samples/Helpdesk.Relational/Incidents/GetDetails/GetIncidentDetailsQueryModel.cs index 2311284..3805748 100644 --- a/samples/Helpdesk.Relational/Incidents/GetDetails/GetIncidentDetailsQueryModel.cs +++ b/samples/Helpdesk.Relational/Incidents/GetDetails/GetIncidentDetailsQueryModel.cs @@ -11,7 +11,7 @@ public class GetIncidentDetailsQueryModel : EntityQueryModel> GetPreconditions() { - yield return IncidentSpecification.OfId(IncidentId); + yield return IncidentSpecification.HasId(IncidentId); } protected override IQueryable ExecuteCore(IQueryable source) => source diff --git a/samples/Helpdesk.Relational/Incidents/Api/v1/GetDetails/GetIncidentDetailsRequest.cs b/samples/Helpdesk.Relational/Incidents/GetDetails/v1/GetIncidentDetailsRequest.cs similarity index 50% rename from samples/Helpdesk.Relational/Incidents/Api/v1/GetDetails/GetIncidentDetailsRequest.cs rename to samples/Helpdesk.Relational/Incidents/GetDetails/v1/GetIncidentDetailsRequest.cs index b31588e..af85c24 100644 --- a/samples/Helpdesk.Relational/Incidents/Api/v1/GetDetails/GetIncidentDetailsRequest.cs +++ b/samples/Helpdesk.Relational/Incidents/GetDetails/v1/GetIncidentDetailsRequest.cs @@ -1,4 +1,4 @@ -namespace Helpdesk.Relational.Incidents.Api.v1.GetDetails; +namespace Helpdesk.Relational.Incidents.GetDetails.v1; public record GetIncidentDetailsRequest( Guid IncidentId); diff --git a/samples/Helpdesk.Relational/Incidents/Api/v1/GetDetails/GetIncidentDetailsHandler.cs b/samples/Helpdesk.Relational/Incidents/GetDetails/v1/GetIncidentDetailsUseCase.cs similarity index 64% rename from samples/Helpdesk.Relational/Incidents/Api/v1/GetDetails/GetIncidentDetailsHandler.cs rename to samples/Helpdesk.Relational/Incidents/GetDetails/v1/GetIncidentDetailsUseCase.cs index f403adb..2578386 100644 --- a/samples/Helpdesk.Relational/Incidents/Api/v1/GetDetails/GetIncidentDetailsHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/GetDetails/v1/GetIncidentDetailsUseCase.cs @@ -1,13 +1,12 @@ -using Helpdesk.Relational.Incidents.GetDetails; -using Raiqub.Expressions.Sessions; +using Raiqub.Expressions.Sessions; -namespace Helpdesk.Relational.Incidents.Api.v1.GetDetails; +namespace Helpdesk.Relational.Incidents.GetDetails.v1; -public class GetIncidentDetailsHandler +public class GetIncidentDetailsUseCase { private readonly IDbQuerySession _dbQuerySession; - public GetIncidentDetailsHandler(IDbQuerySession dbQuerySession) + public GetIncidentDetailsUseCase(IDbQuerySession dbQuerySession) { _dbQuerySession = dbQuerySession; } diff --git a/samples/Helpdesk.Relational/Incidents/GetHistory/IncidentHistory.cs b/samples/Helpdesk.Relational/Incidents/GetHistory/IncidentHistory.cs new file mode 100644 index 0000000..7711bbe --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/GetHistory/IncidentHistory.cs @@ -0,0 +1,7 @@ +namespace Helpdesk.Relational.Incidents.GetHistory; + +public record IncidentHistory( + Guid Id, + Guid IncidentId, + string Description +); diff --git a/samples/Helpdesk.Relational/Incidents/GetHistory/v1/GetIncidentHistoryUseCase.cs b/samples/Helpdesk.Relational/Incidents/GetHistory/v1/GetIncidentHistoryUseCase.cs new file mode 100644 index 0000000..eae041d --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/GetHistory/v1/GetIncidentHistoryUseCase.cs @@ -0,0 +1,75 @@ +using Raiqub.Expressions.Sessions; +using RT.Comb; + +namespace Helpdesk.Relational.Incidents.GetHistory.v1; + +public class GetIncidentHistoryUseCase +{ + private readonly IDbQuerySession _dbQuerySession; + private readonly ICombProvider _combProvider; + + public GetIncidentHistoryUseCase(IDbQuerySession dbQuerySession, ICombProvider combProvider) + { + _dbQuerySession = dbQuerySession; + _combProvider = combProvider; + } + + public async Task> Execute(Guid incidentId, CancellationToken cancellationToken) + { + var incident = await _dbQuerySession + .Query(IncidentSpecification.HasId(incidentId)) + .FirstAsync(cancellationToken); + + return CreateProjection(incident).OrderBy(h => h.Id).ToList(); + } + + private IEnumerable CreateProjection(Incident incident) + { + yield return new IncidentHistory( + _combProvider.Create(incident.Id, incident.LoggedAt.UtcDateTime), + incident.Id, + $"['{incident.LoggedAt}'] Logged Incident with id: '{incident.Id}' for customer '{incident.CustomerId}' and description `{incident.Description}' through {incident.Contact} by '{incident.LoggedBy}'"); + + if (incident.Category is not null) + { + yield return new IncidentHistory( + _combProvider.Create(incident.Id, incident.CategorisedAt!.Value.UtcDateTime), + incident.Id, + $"[{incident.CategorisedAt}] Categorised Incident with id: '{incident.Id}' as {incident.Category} by {incident.CategorisedBy}"); + } + + if (incident.Priority is not null) + { + yield return new IncidentHistory( + _combProvider.Create(incident.Id, incident.PrioritisedAt!.Value.UtcDateTime), + incident.Id, + $"[{incident.PrioritisedAt}] Prioritised Incident with id: '{incident.Id}' as '{incident.Priority}' by {incident.PrioritisedBy}"); + } + + if (incident.AgentId is not null) + { + yield return new IncidentHistory( + _combProvider.Create(incident.Id, incident.AssignedAt!.Value.UtcDateTime), + incident.Id, + $"[{incident.AssignedAt}] Assigned agent `{incident.AgentId} to incident with id: '{incident.Id}'"); + } + + foreach (var response in incident.Responses.OfType()) + { + yield return new IncidentHistory( + _combProvider.Create(incident.Id, response.RespondedAt.UtcDateTime), + incident.Id, + $"[{response.RespondedAt}] Customer '{response.CustomerId}' responded with response '{response.Content}' to Incident with id: '{incident.Id}'"); + } + + foreach (var response in incident.Responses.OfType()) + { + string responseVisibility = response.VisibleToCustomer ? "public" : "private"; + + yield return new IncidentHistory( + _combProvider.Create(incident.Id, response.RespondedAt.UtcDateTime), + incident.Id, + $"[{response.RespondedAt}] Agent '{response.AgentId}' responded with {responseVisibility} response '{response.Content}' to Incident with id: '{incident.Id}'"); + } + } +} diff --git a/samples/Helpdesk.Relational/Incidents/GetShortInfo/GetIncidentShortInfoQueryModel.cs b/samples/Helpdesk.Relational/Incidents/GetShortInfo/GetIncidentShortInfoQueryModel.cs index cdbc892..f21eb30 100644 --- a/samples/Helpdesk.Relational/Incidents/GetShortInfo/GetIncidentShortInfoQueryModel.cs +++ b/samples/Helpdesk.Relational/Incidents/GetShortInfo/GetIncidentShortInfoQueryModel.cs @@ -30,7 +30,7 @@ protected override IQueryable ExecuteCore(IQueryable> Execute(Guid customerId, int? pageNumber, int? pageSize, CancellationToken cancellationToken) + { + return await _dbQuerySession + .Query(new GetIncidentShortInfoQueryModel(customerId, pageNumber ?? 1, pageSize ?? 10)) + .ToListAsync(cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/Incident.cs b/samples/Helpdesk.Relational/Incidents/Incident.cs index be0e846..82dea58 100644 --- a/samples/Helpdesk.Relational/Incidents/Incident.cs +++ b/samples/Helpdesk.Relational/Incidents/Incident.cs @@ -21,10 +21,21 @@ private Incident( string description, Guid loggedBy, DateTimeOffset loggedAt) + : this(id, customerId, description, loggedBy, loggedAt) + { + Contact = contact; + } + + private Incident( + Guid id, + Guid customerId, + string description, + Guid loggedBy, + DateTimeOffset loggedAt) { Id = id; CustomerId = customerId; - Contact = contact; + Contact = null!; Description = description; LoggedBy = loggedBy; LoggedAt = loggedAt; diff --git a/samples/Helpdesk.Relational/Incidents/IncidentResponse.cs b/samples/Helpdesk.Relational/Incidents/IncidentResponse.cs index 798b8e3..c948729 100644 --- a/samples/Helpdesk.Relational/Incidents/IncidentResponse.cs +++ b/samples/Helpdesk.Relational/Incidents/IncidentResponse.cs @@ -5,18 +5,22 @@ public abstract record IncidentResponse public record FromAgent( Guid AgentId, string Content, - bool VisibleToCustomer) - : IncidentResponse(Content); + bool VisibleToCustomer, + DateTimeOffset RespondedAt) + : IncidentResponse(Content, RespondedAt); public record FromCustomer( Guid CustomerId, - string Content) - : IncidentResponse(Content); + string Content, + DateTimeOffset RespondedAt) + : IncidentResponse(Content, RespondedAt); - private IncidentResponse(string content) + private IncidentResponse(string content, DateTimeOffset respondedAt) { Content = content; + RespondedAt = respondedAt; } public string Content { get; init; } + public DateTimeOffset RespondedAt { get; init; } } diff --git a/samples/Helpdesk.Relational/Incidents/IncidentSpecification.cs b/samples/Helpdesk.Relational/Incidents/IncidentSpecification.cs index 3b501dd..a039483 100644 --- a/samples/Helpdesk.Relational/Incidents/IncidentSpecification.cs +++ b/samples/Helpdesk.Relational/Incidents/IncidentSpecification.cs @@ -22,6 +22,6 @@ public static bool IsSatisfiedBy(this Incident incident, Specification public static Specification OfCustomer(Guid customerId) => Specification.Create(incident => incident.CustomerId == customerId); - public static Specification OfId(Guid incidentId) => + public static Specification HasId(Guid incidentId) => Specification.Create(incident => incident.Id == incidentId); } diff --git a/samples/Helpdesk.Relational/Incidents/LogNew/LogHandler.cs b/samples/Helpdesk.Relational/Incidents/LogNew/LogNewScenario.cs similarity index 75% rename from samples/Helpdesk.Relational/Incidents/LogNew/LogHandler.cs rename to samples/Helpdesk.Relational/Incidents/LogNew/LogNewScenario.cs index 456dea6..1fa73e0 100644 --- a/samples/Helpdesk.Relational/Incidents/LogNew/LogHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/LogNew/LogNewScenario.cs @@ -1,8 +1,8 @@ namespace Helpdesk.Relational.Incidents.LogNew; -public static class LogHandler +public static class LogNewScenario { - public static IncidentLogged Handle(LogIncident command) + public static IncidentLogged Execute(LogIncident command) { return new IncidentLogged( command.IncidentId, diff --git a/samples/Helpdesk.Relational/Incidents/LogNew/v1/LogIncidentRequest.cs b/samples/Helpdesk.Relational/Incidents/LogNew/v1/LogIncidentRequest.cs new file mode 100644 index 0000000..a784b67 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/LogNew/v1/LogIncidentRequest.cs @@ -0,0 +1,5 @@ +namespace Helpdesk.Relational.Incidents.LogNew.v1; + +public sealed record LogIncidentRequest( + Contact Contact, + string Description); diff --git a/samples/Helpdesk.Relational/Incidents/LogNew/v1/LogIncidentUseCase.cs b/samples/Helpdesk.Relational/Incidents/LogNew/v1/LogIncidentUseCase.cs new file mode 100644 index 0000000..54154aa --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/LogNew/v1/LogIncidentUseCase.cs @@ -0,0 +1,33 @@ +using Raiqub.Expressions.Sessions; +using RT.Comb; + +namespace Helpdesk.Relational.Incidents.LogNew.v1; + +public class LogIncidentUseCase +{ + private readonly ICombProvider _combProvider; + private readonly IDbSession _dbSession; + + public LogIncidentUseCase(ICombProvider combProvider, IDbSession dbSession) + { + _combProvider = combProvider; + _dbSession = dbSession; + } + + public async Task Execute(Guid customerId, LogIncidentRequest request, CancellationToken cancellationToken) + { + var incident = Incident.Create( + LogNewScenario.Execute( + new LogIncident( + _combProvider.Create(), + customerId, + request.Contact, + request.Description, + customerId))); + + await _dbSession.AddAsync(incident, cancellationToken); + await _dbSession.SaveChangesAsync(cancellationToken); + + return incident; + } +} diff --git a/samples/Helpdesk.Relational/Incidents/Prioritise/PrioritiseHandler.cs b/samples/Helpdesk.Relational/Incidents/Prioritise/PrioritiseScenario.cs similarity index 76% rename from samples/Helpdesk.Relational/Incidents/Prioritise/PrioritiseHandler.cs rename to samples/Helpdesk.Relational/Incidents/Prioritise/PrioritiseScenario.cs index ce89351..117a6ac 100644 --- a/samples/Helpdesk.Relational/Incidents/Prioritise/PrioritiseHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/Prioritise/PrioritiseScenario.cs @@ -2,9 +2,9 @@ namespace Helpdesk.Relational.Incidents.Prioritise; -public static class PrioritiseHandler +public static class PrioritiseScenario { - public static IncidentPrioritised Handle(Incident current, PrioritiseIncident command) + public static IncidentPrioritised Execute(Incident current, PrioritiseIncident command) { if (current.IsSatisfiedBy(IncidentSpecification.IsClosed)) throw new IncidentAlreadyClosedException(command.IncidentId); diff --git a/samples/Helpdesk.Relational/Incidents/Prioritise/v1/PrioritiseIncidentRequest.cs b/samples/Helpdesk.Relational/Incidents/Prioritise/v1/PrioritiseIncidentRequest.cs new file mode 100644 index 0000000..44ab049 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/Prioritise/v1/PrioritiseIncidentRequest.cs @@ -0,0 +1,3 @@ +namespace Helpdesk.Relational.Incidents.Prioritise.v1; + +public record PrioritiseIncidentRequest(IncidentPriority Priority); diff --git a/samples/Helpdesk.Relational/Incidents/Prioritise/v1/PrioritiseIncidentUseCase.cs b/samples/Helpdesk.Relational/Incidents/Prioritise/v1/PrioritiseIncidentUseCase.cs new file mode 100644 index 0000000..3d5c2e9 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/Prioritise/v1/PrioritiseIncidentUseCase.cs @@ -0,0 +1,27 @@ +using Raiqub.Expressions.Sessions; + +namespace Helpdesk.Relational.Incidents.Prioritise.v1; + +public class PrioritiseIncidentUseCase +{ + private readonly IDbSession _dbSession; + + public PrioritiseIncidentUseCase(IDbSession dbSession) + { + _dbSession = dbSession; + } + + public async Task Execute( + Guid incidentId, + Guid agentId, + PrioritiseIncidentRequest request, + CancellationToken cancellationToken) + { + var incident = await _dbSession.Query(IncidentSpecification.HasId(incidentId)).FirstAsync(cancellationToken); + incident.Apply( + PrioritiseScenario.Execute(incident, new PrioritiseIncident(incidentId, request.Priority, agentId))); + + _dbSession.Update(incident); + await _dbSession.SaveChangesAsync(cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/RecordAgentResponseHandler.cs b/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/RecordAgentResponseScenario.cs similarity index 70% rename from samples/Helpdesk.Relational/Incidents/RecordAgentResponse/RecordAgentResponseHandler.cs rename to samples/Helpdesk.Relational/Incidents/RecordAgentResponse/RecordAgentResponseScenario.cs index 402234d..5fca497 100644 --- a/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/RecordAgentResponseHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/RecordAgentResponseScenario.cs @@ -2,9 +2,9 @@ namespace Helpdesk.Relational.Incidents.RecordAgentResponse; -public static class RecordAgentResponseHandler +public static class RecordAgentResponseScenario { - public static AgentRespondedToIncident Handle(Incident current, RecordAgentResponseToIncident command) + public static AgentRespondedToIncident Execute(Incident current, RecordAgentResponseToIncident command) { if (current.IsSatisfiedBy(IncidentSpecification.IsClosed)) throw new IncidentAlreadyClosedException(command.IncidentId); diff --git a/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/v1/RecordAgentResponseToIncidentRequest.cs b/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/v1/RecordAgentResponseToIncidentRequest.cs new file mode 100644 index 0000000..c8c2dba --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/v1/RecordAgentResponseToIncidentRequest.cs @@ -0,0 +1,5 @@ +namespace Helpdesk.Relational.Incidents.RecordAgentResponse.v1; + +public record RecordAgentResponseToIncidentRequest( + string Content, + bool VisibleToCustomer); diff --git a/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/v1/RecordAgentResponseToIncidentUseCase.cs b/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/v1/RecordAgentResponseToIncidentUseCase.cs new file mode 100644 index 0000000..b212f24 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/RecordAgentResponse/v1/RecordAgentResponseToIncidentUseCase.cs @@ -0,0 +1,35 @@ +using Raiqub.Expressions.Sessions; + +namespace Helpdesk.Relational.Incidents.RecordAgentResponse.v1; + +public class RecordAgentResponseToIncidentUseCase +{ + private readonly IDbSession _dbSession; + + public RecordAgentResponseToIncidentUseCase(IDbSession dbSession) + { + _dbSession = dbSession; + } + + public async Task Execute( + Guid incidentId, + Guid agentId, + RecordAgentResponseToIncidentRequest request, + CancellationToken cancellationToken) + { + var incident = await _dbSession.Query(IncidentSpecification.HasId(incidentId)).FirstAsync(cancellationToken); + incident.Apply( + RecordAgentResponseScenario.Execute( + incident, + new RecordAgentResponseToIncident( + incidentId, + new IncidentResponse.FromAgent( + agentId, + request.Content, + request.VisibleToCustomer, + DateTimeOffset.UtcNow)))); + + _dbSession.Update(incident); + await _dbSession.SaveChangesAsync(cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/RecordCustomerResponseHandler.cs b/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/RecordCustomerResponseScenario.cs similarity index 69% rename from samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/RecordCustomerResponseHandler.cs rename to samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/RecordCustomerResponseScenario.cs index f5ed00a..0b006a3 100644 --- a/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/RecordCustomerResponseHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/RecordCustomerResponseScenario.cs @@ -2,9 +2,9 @@ namespace Helpdesk.Relational.Incidents.RecordCustomerResponse; -public static class RecordCustomerResponseHandler +public static class RecordCustomerResponseScenario { - public static CustomerRespondedToIncident Handle(Incident current, RecordCustomerResponseToIncident command) + public static CustomerRespondedToIncident Execute(Incident current, RecordCustomerResponseToIncident command) { if (current.IsSatisfiedBy(IncidentSpecification.IsClosed)) throw new IncidentAlreadyClosedException(command.IncidentId); diff --git a/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/v1/RecordCustomerResponseToIncidentRequest.cs b/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/v1/RecordCustomerResponseToIncidentRequest.cs new file mode 100644 index 0000000..06071da --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/v1/RecordCustomerResponseToIncidentRequest.cs @@ -0,0 +1,3 @@ +namespace Helpdesk.Relational.Incidents.RecordCustomerResponse.v1; + +public record RecordCustomerResponseToIncidentRequest(string Content); diff --git a/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/v1/RecordCustomerResponseToIncidentUseCase.cs b/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/v1/RecordCustomerResponseToIncidentUseCase.cs new file mode 100644 index 0000000..dedce6d --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/RecordCustomerResponse/v1/RecordCustomerResponseToIncidentUseCase.cs @@ -0,0 +1,31 @@ +using Raiqub.Expressions.Sessions; + +namespace Helpdesk.Relational.Incidents.RecordCustomerResponse.v1; + +public class RecordCustomerResponseToIncidentUseCase +{ + private readonly IDbSession _dbSession; + + public RecordCustomerResponseToIncidentUseCase(IDbSession dbSession) + { + _dbSession = dbSession; + } + + public async Task Execute( + Guid incidentId, + Guid customerId, + RecordCustomerResponseToIncidentRequest request, + CancellationToken cancellationToken) + { + var incident = await _dbSession.Query(IncidentSpecification.HasId(incidentId)).FirstAsync(cancellationToken); + incident.Apply( + RecordCustomerResponseScenario.Execute( + incident, + new RecordCustomerResponseToIncident( + incidentId, + new IncidentResponse.FromCustomer(customerId, request.Content, DateTimeOffset.UtcNow)))); + + _dbSession.Update(incident); + await _dbSession.SaveChangesAsync(cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Incidents/Resolve/ResolveHandler.cs b/samples/Helpdesk.Relational/Incidents/Resolve/ResolveScenario.cs similarity index 82% rename from samples/Helpdesk.Relational/Incidents/Resolve/ResolveHandler.cs rename to samples/Helpdesk.Relational/Incidents/Resolve/ResolveScenario.cs index 0284496..813aefd 100644 --- a/samples/Helpdesk.Relational/Incidents/Resolve/ResolveHandler.cs +++ b/samples/Helpdesk.Relational/Incidents/Resolve/ResolveScenario.cs @@ -1,8 +1,8 @@ namespace Helpdesk.Relational.Incidents.Resolve; -public static class ResolveHandler +public static class ResolveScenario { - public static IncidentResolved Handle(Incident current, ResolveIncident command) + public static IncidentResolved Execute(Incident current, ResolveIncident command) { if (current.IsSatisfiedBy(IncidentSpecification.IsResolvedOrClosed)) throw new IncidentAlreadyResolvedOrClosedWhenResolvingException(command.IncidentId); diff --git a/samples/Helpdesk.Relational/Incidents/Resolve/v1/ResolveIncidentRequest.cs b/samples/Helpdesk.Relational/Incidents/Resolve/v1/ResolveIncidentRequest.cs new file mode 100644 index 0000000..f0000ba --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/Resolve/v1/ResolveIncidentRequest.cs @@ -0,0 +1,3 @@ +namespace Helpdesk.Relational.Incidents.Resolve.v1; + +public record ResolveIncidentRequest(ResolutionType Resolution); diff --git a/samples/Helpdesk.Relational/Incidents/Resolve/v1/ResolveIncidentUseCase.cs b/samples/Helpdesk.Relational/Incidents/Resolve/v1/ResolveIncidentUseCase.cs new file mode 100644 index 0000000..7a5cd63 --- /dev/null +++ b/samples/Helpdesk.Relational/Incidents/Resolve/v1/ResolveIncidentUseCase.cs @@ -0,0 +1,26 @@ +using Raiqub.Expressions.Sessions; + +namespace Helpdesk.Relational.Incidents.Resolve.v1; + +public class ResolveIncidentUseCase +{ + private readonly IDbSession _dbSession; + + public ResolveIncidentUseCase(IDbSession dbSession) + { + _dbSession = dbSession; + } + + public async Task Execute( + Guid incidentId, + Guid agentId, + ResolveIncidentRequest request, + CancellationToken cancellationToken) + { + var incident = await _dbSession.Query(IncidentSpecification.HasId(incidentId)).FirstAsync(cancellationToken); + incident.Apply(ResolveScenario.Execute(incident, new ResolveIncident(incidentId, request.Resolution, agentId))); + + _dbSession.Update(incident); + await _dbSession.SaveChangesAsync(cancellationToken); + } +} diff --git a/samples/Helpdesk.Relational/Program.cs b/samples/Helpdesk.Relational/Program.cs new file mode 100644 index 0000000..7e7841d --- /dev/null +++ b/samples/Helpdesk.Relational/Program.cs @@ -0,0 +1,186 @@ +using System.Text.Json.Serialization; +using Helpdesk.Relational; +using Helpdesk.Relational.Incidents.Acknowledge.v1; +using Helpdesk.Relational.Incidents.AssignAgent.v1; +using Helpdesk.Relational.Incidents.Categorise.v1; +using Helpdesk.Relational.Incidents.Close.v1; +using Helpdesk.Relational.Incidents.GetCustomerSummary.v1; +using Helpdesk.Relational.Incidents.GetDetails.v1; +using Helpdesk.Relational.Incidents.GetHistory.v1; +using Helpdesk.Relational.Incidents.GetShortInfo.v1; +using Helpdesk.Relational.Incidents.LogNew.v1; +using Helpdesk.Relational.Incidents.Prioritise.v1; +using Helpdesk.Relational.Incidents.RecordAgentResponse.v1; +using Helpdesk.Relational.Incidents.RecordCustomerResponse.v1; +using Helpdesk.Relational.Incidents.Resolve.v1; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Raiqub.Expressions.EntityFrameworkCore; +using RT.Comb; +using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddEndpointsApiExplorer() + .AddSwaggerGen() + .AddSingleton(Provider.PostgreSql) + .Configure(options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())) + .Configure( + options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); + +// Entity Framework +builder.Services + .AddDbContextFactory(ob => ob.UseNpgsql(builder.Configuration.GetConnectionString("Incidents"))) + .AddEntityFrameworkExpressions() + .AddSingleContext(); + +// Handlers +builder.Services + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger() + .UseSwaggerUI(); +} + +app.MapPost( + "api/customers/{customerId:guid}/incidents/", + async (LogIncidentUseCase useCase, Guid customerId, LogIncidentRequest request, CancellationToken ct) => + { + var incident = await useCase.Execute(customerId, request, ct); + return Results.Created($"api/incidents/{incident.Id}", incident.Id); + }) + .WithTags("Customer"); + +app.MapPost( + "api/agents/{agentId:guid}/incidents/{incidentId:guid}/category", + ( + CategoriseIncidentUseCase useCase, + Guid incidentId, + Guid agentId, + CategoriseIncidentRequest request, + CancellationToken ct) => + useCase.Execute(incidentId, agentId, request, ct)) + .WithTags("Agent"); + +app.MapPost( + "api/agents/{agentId:guid}/incidents/{incidentId:guid}/priority", + ( + PrioritiseIncidentUseCase useCase, + Guid incidentId, + Guid agentId, + PrioritiseIncidentRequest request, + CancellationToken ct) => useCase.Execute(incidentId, agentId, request, ct)) + .WithTags("Agent"); + +app.MapPost( + "api/agents/{agentId:guid}/incidents/{incidentId:guid}/assign", + ( + AssignAgentToIncidentUseCase useCase, + Guid incidentId, + Guid agentId, + CancellationToken ct) => useCase.Execute(incidentId, agentId, ct)) + .WithTags("Agent"); + +app.MapPost( + "api/customers/{customerId:guid}/incidents/{incidentId:guid}/responses/", + ( + RecordCustomerResponseToIncidentUseCase useCase, + Guid incidentId, + Guid customerId, + RecordCustomerResponseToIncidentRequest request, + CancellationToken ct) => useCase.Execute(incidentId, customerId, request, ct)) + .WithTags("Customer"); + +app.MapPost( + "api/agents/{agentId:guid}/incidents/{incidentId:guid}/responses/", + ( + RecordAgentResponseToIncidentUseCase useCase, + Guid incidentId, + Guid agentId, + RecordAgentResponseToIncidentRequest request, + CancellationToken ct) => useCase.Execute(incidentId, agentId, request, ct)) + .WithTags("Agent"); + +app.MapPost( + "api/agents/{agentId:guid}/incidents/{incidentId:guid}/resolve", + ( + ResolveIncidentUseCase useCase, + Guid incidentId, + Guid agentId, + ResolveIncidentRequest request, + CancellationToken ct) => useCase.Execute(incidentId, agentId, request, ct)) + .WithTags("Agent"); + +app.MapPost( + "api/customers/{customerId:guid}/incidents/{incidentId:guid}/acknowledge", + ( + AcknowledgeIncidentResolutionUseCase useCase, + Guid incidentId, + Guid customerId, + CancellationToken ct) => useCase.Execute(incidentId, customerId, ct)) + .WithTags("Customer"); + +app.MapPost( + "api/agents/{agentId:guid}/incidents/{incidentId:guid}/close", + async ( + CloseIncidentUseCase useCase, + Guid incidentId, + Guid agentId, + CancellationToken ct) => + { + await useCase.Execute(incidentId, agentId, ct); + return Results.Ok(); + }) + .WithTags("Agent"); + +app.MapGet( + "api/customers/{customerId:guid}/incidents/", + ( + GetIncidentShortInfoUseCase useCase, + Guid customerId, + [FromQuery] int? pageNumber, + [FromQuery] int? pageSize, + CancellationToken ct) => + useCase.Execute(customerId, pageNumber, pageSize, ct)) + .WithTags("Customer"); + +app.MapGet( + "api/incidents/{incidentId:guid}", + (GetIncidentDetailsUseCase useCase, Guid incidentId, CancellationToken ct) => + useCase.Execute(new GetIncidentDetailsRequest(incidentId), ct)) + .WithTags("Incident"); + +app.MapGet( + "api/incidents/{incidentId:guid}/history", + (GetIncidentHistoryUseCase useCase, Guid incidentId, CancellationToken ct) => useCase.Execute(incidentId, ct)) + .WithTags("Incident"); + +app.MapGet( + "api/customers/{customerId:guid}/incidents-summary", + (GetCustomerIncidentsSummaryUseCase useCase, Guid customerId, CancellationToken ct) => + useCase.Execute(customerId, ct)) + .WithTags("Customer"); + +using (var scope = app.Services.CreateScope()) +{ + scope.ServiceProvider.GetRequiredService().Database.EnsureCreated(); +} + +app.Run(); diff --git a/samples/Helpdesk.Relational/appsettings.json b/samples/Helpdesk.Relational/appsettings.json new file mode 100644 index 0000000..90257c4 --- /dev/null +++ b/samples/Helpdesk.Relational/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "ConnectionStrings": { + "Incidents": "Host=helpdesk-relational-db;Port=5432;Timeout=15;Pooling=true;MinPoolSize=1;MaxPoolSize=100;CommandTimeout=20;Database='postgres';Password='Password12!';User ID='postgres'" + } +}