Skip to content

Commit 6e37473

Browse files
authored
Add Create method to SupportTaskService (#2868)
We're moving to DB-generated IDs for support tasks. The older style event generation doesn't work with DB-generated data as the event is created before the row is saved to the DB. As such, we need to move support task generation to the newer style events. This is the first step - adding a `CreateSupportTaskAsync` method to the `SupportTaskService`. I've also added tests for the `Delete` method.
1 parent c74e281 commit 6e37473

File tree

8 files changed

+233
-10
lines changed

8 files changed

+233
-10
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using TeachingRecordSystem.Core.Models.SupportTasks;
2+
3+
namespace TeachingRecordSystem.Core.Services.SupportTasks;
4+
5+
public record CreateSupportTaskOptions
6+
{
7+
public required SupportTaskType SupportTaskType { get; init; }
8+
public required ISupportTaskData Data { get; init; }
9+
public required Guid? PersonId { get; init; }
10+
public required string? OneLoginUserSubject { get; init; }
11+
public required (Guid ApplicationUserId, string RequestId)? TrnRequest { get; init; }
12+
}

TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/SupportTasks/SupportTaskService.cs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,57 @@
11
using TeachingRecordSystem.Core.DataStore.Postgres;
2+
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
23

34
namespace TeachingRecordSystem.Core.Services.SupportTasks;
45

5-
public class SupportTaskService(TrsDbContext dbContext, IEventPublisher eventPublisher, IClock clock)
6+
public class SupportTaskService(TrsDbContext dbContext, IEventPublisher eventPublisher)
67
{
7-
public virtual async Task<DeleteSupportTaskResult> DeleteSupportTaskAsync(DeleteSupportTaskOptions options, ProcessContext processContext)
8+
public async Task<SupportTask> CreateSupportTaskAsync(CreateSupportTaskOptions options, ProcessContext processContext)
9+
{
10+
if (options.SupportTaskType.GetDataType() != options.Data.GetType())
11+
{
12+
throw new InvalidOperationException(
13+
$"{nameof(options.Data)} type '{options.Data.GetType()}' is not valid for the specified {nameof(SupportTaskType)}.");
14+
}
15+
16+
var supportTask = new SupportTask
17+
{
18+
SupportTaskReference = SupportTask.GenerateSupportTaskReference(),
19+
CreatedOn = processContext.Now,
20+
UpdatedOn = processContext.Now,
21+
SupportTaskType = options.SupportTaskType,
22+
Status = SupportTaskStatus.Open,
23+
Data = options.Data,
24+
PersonId = options.PersonId,
25+
OneLoginUserSubject = options.OneLoginUserSubject,
26+
TrnRequestApplicationUserId = options.TrnRequest?.ApplicationUserId,
27+
TrnRequestId = options.TrnRequest?.RequestId
28+
};
29+
30+
dbContext.SupportTasks.Add(supportTask);
31+
32+
await dbContext.SaveChangesAsync();
33+
34+
await eventPublisher.PublishEventAsync(
35+
new SupportTaskCreatedEvent
36+
{
37+
EventId = Guid.NewGuid(),
38+
SupportTask = EventModels.SupportTask.FromModel(supportTask)
39+
},
40+
processContext);
41+
42+
return supportTask;
43+
}
44+
45+
public async Task<DeleteSupportTaskResult> DeleteSupportTaskAsync(DeleteSupportTaskOptions options, ProcessContext processContext)
846
{
947
var supportTask = await dbContext.SupportTasks.FindAsync(options.SupportTaskReference);
1048
if (supportTask is null)
1149
{
1250
return DeleteSupportTaskResult.NotFound;
1351
}
1452

15-
supportTask.DeletedOn = clock.UtcNow;
53+
supportTask.DeletedOn = processContext.Now;
54+
1655
await dbContext.SaveChangesAsync();
1756

1857
await eventPublisher.PublishEventAsync(

TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Jobs/SendTrnRecipientEmailJobTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public async Task ExecuteAsync_SendsEmailAndPublishesEvent()
5151
email = await WithDbContextAsync(dbContext => dbContext.Emails.SingleAsync(e => e.EmailId == email.EmailId));
5252
Assert.Equal(Clock.UtcNow, email.SentOn);
5353

54-
Events.AssertEventsPublished(x =>
54+
Events.AssertProcessesAndEventsPublished(x =>
5555
{
5656
Assert.Equal(ProcessType.NotifyingTrnRecipient, x.ProcessContext.ProcessType);
5757
Assert.Collection(x.ProcessContext.Process.PersonIds, id => Assert.Equal(person.PersonId, id));

TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/Notes/NoteServiceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ await WithDbContextAsync(async dbContext =>
4242
Assert.Equal(options.OriginalFileName, dbNote.OriginalFileName);
4343
});
4444

45-
Events.AssertEventsPublished(x =>
45+
Events.AssertProcessesAndEventsPublished(x =>
4646
{
4747
var noteCreatedEvent = Assert.IsType<NoteCreatedEvent>(x.Event);
4848
Assert.Equal(person.PersonId, noteCreatedEvent.PersonId);
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
2+
using TeachingRecordSystem.Core.Models.SupportTasks;
3+
using TeachingRecordSystem.Core.Services.SupportTasks;
4+
5+
namespace TeachingRecordSystem.Core.Tests.Services.SupportTasks;
6+
7+
public class SupportTaskServiceTests(ServiceFixture fixture) : ServiceTestBase(fixture)
8+
{
9+
[Fact]
10+
public async Task CreateSupportTaskAsync_InvalidDataType_ThrowsInvalidOperationException()
11+
{
12+
// Arrange
13+
var person = await TestData.CreatePersonAsync(p => p.WithEmailAddress());
14+
15+
var options = new CreateSupportTaskOptions
16+
{
17+
SupportTaskType = SupportTaskType.ChangeNameRequest,
18+
Data = new ChangeDateOfBirthRequestData()
19+
{
20+
DateOfBirth = TestData.GenerateChangedDateOfBirth(person.DateOfBirth),
21+
EvidenceFileId = Guid.NewGuid(),
22+
EvidenceFileName = "evidence.jpeg",
23+
EmailAddress = person.EmailAddress!,
24+
ChangeRequestOutcome = null
25+
},
26+
PersonId = person.PersonId,
27+
OneLoginUserSubject = null,
28+
TrnRequest = null
29+
};
30+
31+
var processContext = new ProcessContext(default, Clock.UtcNow, SystemUser.SystemUserId);
32+
33+
// Act
34+
var ex = await Record.ExceptionAsync(() => WithServiceAsync<SupportTaskService, SupportTask>(
35+
service => service.CreateSupportTaskAsync(options, processContext)));
36+
37+
// Assert
38+
Assert.IsType<InvalidOperationException>(ex);
39+
}
40+
41+
[Fact]
42+
public async Task CreateSupportTaskAsync_ValidRequest_CreatesSupportTaskAndPublishesEvent()
43+
{
44+
// Arrange
45+
var person = await TestData.CreatePersonAsync(p => p.WithEmailAddress());
46+
47+
var options = new CreateSupportTaskOptions
48+
{
49+
SupportTaskType = SupportTaskType.ChangeNameRequest,
50+
Data = new ChangeNameRequestData
51+
{
52+
FirstName = person.FirstName,
53+
MiddleName = person.MiddleName,
54+
LastName = TestData.GenerateChangedLastName(person.LastName),
55+
EvidenceFileId = Guid.NewGuid(),
56+
EvidenceFileName = "evidence.jpeg",
57+
EmailAddress = person.EmailAddress!,
58+
ChangeRequestOutcome = null
59+
},
60+
PersonId = person.PersonId,
61+
OneLoginUserSubject = null,
62+
TrnRequest = null
63+
};
64+
65+
var processContext = new ProcessContext(default, Clock.UtcNow, SystemUser.SystemUserId);
66+
67+
// Act
68+
var result = await WithServiceAsync<SupportTaskService, SupportTask>(
69+
service => service.CreateSupportTaskAsync(options, processContext));
70+
71+
// Assert
72+
Assert.NotNull(result.SupportTaskReference);
73+
Assert.Equal(options.SupportTaskType, result.SupportTaskType);
74+
Assert.Equal(options.Data, result.Data);
75+
Assert.Equal(options.PersonId, result.PersonId);
76+
Assert.Equal(SupportTaskStatus.Open, result.Status);
77+
78+
Events.AssertEventsPublished(e =>
79+
{
80+
var supportTaskCreatedEvent = Assert.IsType<SupportTaskCreatedEvent>(e);
81+
Assert.Equal(result.SupportTaskReference, supportTaskCreatedEvent.SupportTask.SupportTaskReference);
82+
Assert.Equal(result.SupportTaskType, supportTaskCreatedEvent.SupportTask.SupportTaskType);
83+
Assert.Equal(result.PersonId, supportTaskCreatedEvent.SupportTask.PersonId);
84+
Assert.Equal(result.Data, supportTaskCreatedEvent.SupportTask.Data);
85+
Assert.Equal(result.Status, supportTaskCreatedEvent.SupportTask.Status);
86+
});
87+
}
88+
89+
[Fact]
90+
public async Task DeleteSupportTaskAsync_TaskDoesNotExist_ReturnsNotFoundAndDoesNotPublishEvent()
91+
{
92+
// Arrange
93+
var supportTaskReference = "ABC-123";
94+
95+
var reasonDetail = Faker.Lorem.Paragraph();
96+
var options = new DeleteSupportTaskOptions(supportTaskReference, reasonDetail);
97+
98+
var processContext = new ProcessContext(default, Clock.UtcNow, SystemUser.SystemUserId);
99+
100+
// Act
101+
var result = await WithServiceAsync<SupportTaskService, DeleteSupportTaskResult>(
102+
service => service.DeleteSupportTaskAsync(options, processContext));
103+
104+
// Assert
105+
Assert.Equal(DeleteSupportTaskResult.NotFound, result);
106+
Events.AssertNoEventsPublished();
107+
}
108+
109+
[Fact]
110+
public async Task DeleteSupportTaskAsync_TaskIsAlreadyDeleted_ReturnsNotFoundAndDoesNotPublishEvent()
111+
{
112+
// Arrange
113+
var person = await TestData.CreatePersonAsync();
114+
var supportTask = await TestData.CreateChangeNameRequestSupportTaskAsync(person.PersonId);
115+
116+
await WithDbContextAsync(async dbContext =>
117+
{
118+
dbContext.Attach(supportTask);
119+
supportTask.DeletedOn = Clock.UtcNow.AddDays(-1);
120+
await dbContext.SaveChangesAsync();
121+
});
122+
123+
var reasonDetail = Faker.Lorem.Paragraph();
124+
var options = new DeleteSupportTaskOptions(supportTask.SupportTaskReference, reasonDetail);
125+
126+
var processContext = new ProcessContext(default, Clock.UtcNow, SystemUser.SystemUserId);
127+
128+
// Act
129+
var result = await WithServiceAsync<SupportTaskService, DeleteSupportTaskResult>(
130+
service => service.DeleteSupportTaskAsync(options, processContext));
131+
132+
// Assert
133+
Assert.Equal(DeleteSupportTaskResult.NotFound, result);
134+
Events.AssertNoEventsPublished();
135+
}
136+
137+
[Fact]
138+
public async Task DeleteSupportTaskAsync_ValidRequest_DeletesTaskAndPublishesEvent()
139+
{
140+
// Arrange
141+
var person = await TestData.CreatePersonAsync();
142+
var supportTask = await TestData.CreateChangeNameRequestSupportTaskAsync(person.PersonId);
143+
144+
var reasonDetail = Faker.Lorem.Paragraph();
145+
var options = new DeleteSupportTaskOptions(supportTask.SupportTaskReference, reasonDetail);
146+
147+
var processContext = new ProcessContext(default, Clock.UtcNow, SystemUser.SystemUserId);
148+
149+
// Act
150+
var result = await WithServiceAsync<SupportTaskService, DeleteSupportTaskResult>(
151+
service => service.DeleteSupportTaskAsync(options, processContext));
152+
153+
// Assert
154+
Assert.Equal(DeleteSupportTaskResult.Ok, result);
155+
156+
await WithDbContextAsync(async dbContext =>
157+
{
158+
var dbSupportTask = await dbContext.SupportTasks.IgnoreQueryFilters().SingleAsync(t => t.SupportTaskReference == supportTask.SupportTaskReference);
159+
Assert.Equal(processContext.Now, dbSupportTask.DeletedOn);
160+
});
161+
162+
Events.AssertEventsPublished(e =>
163+
{
164+
var supportTaskDeletedEvent = Assert.IsType<SupportTaskDeletedEvent>(e);
165+
Assert.Equal(supportTask.SupportTaskReference, supportTaskDeletedEvent.SupportTaskReference);
166+
Assert.Equal(reasonDetail, supportTaskDeletedEvent.ReasonDetail);
167+
});
168+
}
169+
}

TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/TestScopedServices.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public TestScopedServices()
2020
public static void ConfigureServices(IServiceCollection services) =>
2121
services
2222
.AddSingleton<IClock>(new ForwardToTestScopedClock())
23-
.AddSingleton<EventCapture>()
23+
.AddTestScoped<EventCapture>(tss => tss.Events)
2424
.AddTransient<IEventHandler>(sp => sp.GetRequiredService<EventCapture>());
2525

2626
public static TestScopedServices GetCurrent() =>

TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/AddNoteTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public async Task Post_ContentWithoutFile_CreatesNoteAndEventAndRedirects()
5353
Assert.Equal(content, note.Content);
5454
Assert.Null(note.FileId);
5555

56-
Events.AssertEventsPublished(x =>
56+
Events.AssertProcessesAndEventsPublished(x =>
5757
{
5858
Assert.Equal(ProcessType.NoteCreating, x.ProcessContext.ProcessType);
5959
Assert.IsType<NoteCreatedEvent>(x.Event);
@@ -89,7 +89,7 @@ public async Task Post_ContentWithFile_CreatesNoteAndEventAndRedirects()
8989
Assert.Equal(content, note.Content);
9090
Assert.NotNull(note.FileId);
9191

92-
Events.AssertEventsPublished(x =>
92+
Events.AssertProcessesAndEventsPublished(x =>
9393
{
9494
Assert.Equal(ProcessType.NoteCreating, x.ProcessContext.ProcessType);
9595
Assert.IsType<NoteCreatedEvent>(x.Event);

TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/EventCapture.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ public class EventCapture : IEventHandler
88

99
public void Clear() => _events.Clear();
1010

11-
public void AssertEventsPublished(params Action<EventAndProcess>[] eventInspectors) =>
12-
Assert.Collection(_events, eventInspectors);
11+
public void AssertEventsPublished(params Action<IEvent>[] eventInspectors) =>
12+
Assert.Collection(_events.Select(t => t.Event), eventInspectors);
13+
14+
public void AssertProcessesAndEventsPublished(params Action<EventAndProcess>[] eventAndProcessInspectors) =>
15+
Assert.Collection(_events, eventAndProcessInspectors);
1316

1417
public void AssertNoEventsPublished() =>
1518
Assert.Empty(_events);

0 commit comments

Comments
 (0)