Skip to content

Commit

Permalink
Update CorrespondenceRequest model (#926)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielskovli authored Nov 25, 2024
1 parent 723992f commit 78cbeb0
Show file tree
Hide file tree
Showing 12 changed files with 352 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class CorrespondenceRequestBuilder : ICorrespondenceRequestBuilder
private List<CorrespondenceReplyOption>? _replyOptions;
private CorrespondenceNotification? _notification;
private bool? _ignoreReservation;
private bool? _isConfirmationNeeded;
private List<Guid>? _existingAttachments;

private CorrespondenceRequestBuilder() { }
Expand Down Expand Up @@ -247,6 +248,13 @@ public ICorrespondenceRequestBuilder WithIgnoreReservation(bool ignoreReservatio
return this;
}

/// <inheritdoc/>
public ICorrespondenceRequestBuilder WithIsConfirmationNeeded(bool isConfirmationNeeded)
{
_isConfirmationNeeded = isConfirmationNeeded;
return this;
}

/// <inheritdoc/>
public ICorrespondenceRequestBuilder WithExistingAttachment(Guid existingAttachment)
{
Expand Down Expand Up @@ -308,6 +316,7 @@ public CorrespondenceRequest Build()
Notification = _notification,
IgnoreReservation = _ignoreReservation,
ExistingAttachments = _existingAttachments,
IsConfirmationNeeded = _isConfirmationNeeded,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,15 @@ IEnumerable<CorrespondenceExternalReference> externalReferences
/// <summary>
/// Sets whether the correspondence can override reservation against digital communication in KRR
/// </summary>
/// <param name="ignoreReservation">A boolean value indicating whether or not reservations can be ignored</param>
/// <param name="ignoreReservation">A boolean value indicating if reservations can be ignored or not</param>
ICorrespondenceRequestBuilder WithIgnoreReservation(bool ignoreReservation);

/// <summary>
/// Sets whether reading the correspondence needs to be confirmed by the recipient
/// </summary>
/// <param name="isConfirmationNeeded">A boolean value indicating if confirmation is needed or not</param>
ICorrespondenceRequestBuilder WithIsConfirmationNeeded(bool isConfirmationNeeded);

/// <summary>
/// Adds an existing attachment reference to the correspondence
/// <remarks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public CorrespondenceClient(
_authorisationFactory = new CorrespondenceAuthorisationFactory(serviceProvider);
}

private async Task<JwtToken> AuthorisationFactory(CorrespondencePayloadBase payload)
private async Task<JwtToken> AuthorisationResolver(CorrespondencePayloadBase payload)
{
if (payload.AccessTokenFactory is null && payload.AuthorisationMethod is null)
{
Expand Down Expand Up @@ -162,7 +162,7 @@ CorrespondencePayloadBase payload
)
{
_logger.LogDebug("Fetching access token via factory");
JwtToken accessToken = await AuthorisationFactory(payload);
JwtToken accessToken = await AuthorisationResolver(payload);

_logger.LogDebug("Constructing authorized http request for target uri {TargetEndpoint}", uri);
HttpRequestMessage request = new(method, uri) { Content = content };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ namespace Altinn.App.Core.Features.Correspondence;
public interface ICorrespondenceClient
{
/// <summary>
/// Sends a correspondence
/// <para>Sends a correspondence.</para>
/// <para>After a successful request, the state of the correspondence order is <see cref="CorrespondenceStatus.Initialized"/>.
/// This indicates that the request has met all validation requirements and is considered valid, but until the state
/// reaches <see cref="CorrespondenceStatus.Published"/> it has not actually been sent to the recipient.</para>
/// <para>The current status of a correspondence and the associated notifications can be checked via <see cref="GetStatus"/>.</para>
/// <para>Alternatively, the correspondence service publishes events which can be subscribed to.
/// For more information, see https://docs.altinn.studio/correspondence/getting-started/developer-guides/events/</para>
/// </summary>
/// <param name="payload">The <see cref="SendCorrespondencePayload"/> payload</param>
/// <param name="cancellationToken">An optional cancellation token</param>
/// <returns></returns>
Task<SendCorrespondenceResponse> Send(
SendCorrespondencePayload payload,
CancellationToken cancellationToken = default
Expand All @@ -23,7 +28,6 @@ Task<SendCorrespondenceResponse> Send(
/// </summary>
/// <param name="payload">The <see cref="GetCorrespondenceStatusPayload"/> payload</param>
/// <param name="cancellationToken">An optional cancellation token</param>
/// <returns></returns>
Task<GetCorrespondenceStatusResponse> GetStatus(
GetCorrespondenceStatusPayload payload,
CancellationToken cancellationToken = default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ internal void Serialise(MultipartFormDataContent content)
AddIfNotNull(content, ReminderSmsBody, "Correspondence.Notification.ReminderSmsBody");
AddIfNotNull(content, NotificationChannel.ToString(), "Correspondence.Notification.NotificationChannel");
AddIfNotNull(content, SendersReference, "Correspondence.Notification.SendersReference");
AddIfNotNull(content, RequestedSendTime?.ToString("O"), "Correspondence.Notification.RequestedSendTime");
AddIfNotNull(content, RequestedSendTime, "Correspondence.Notification.RequestedSendTime");
AddIfNotNull(
content,
ReminderNotificationChannel.ToString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ internal static void AddRequired(MultipartFormDataContent content, string value,
content.Add(new StringContent(value), name);
}

internal static void AddRequired(MultipartFormDataContent content, DateTimeOffset value, string name)
{
if (value == default)
throw new CorrespondenceValueException($"Required value is missing: {name}");

var normalisedAndFormatted = NormaliseDateTime(value).ToString("O");
content.Add(new StringContent(normalisedAndFormatted), name);
}

internal static void AddRequired(
MultipartFormDataContent content,
ReadOnlyMemory<byte> data,
Expand All @@ -37,6 +46,15 @@ internal static void AddIfNotNull(MultipartFormDataContent content, string? valu
content.Add(new StringContent(value), name);
}

internal static void AddIfNotNull(MultipartFormDataContent content, DateTimeOffset? value, string name)
{
if (value is null)
return;

var normalisedAndFormatted = NormaliseDateTime(value.Value).ToString("O");
content.Add(new StringContent(normalisedAndFormatted), name);
}

internal static void AddListItems<T>(
MultipartFormDataContent content,
IReadOnlyList<T>? items,
Expand Down Expand Up @@ -153,6 +171,14 @@ internal void ValidateAllProperties(string dataTypeName)
);
}
}

/// <summary>
/// Removes the <see cref="DateTimeOffset.Ticks"/> portion of a <see cref="DateTimeOffset"/>
/// </summary>
internal static DateTimeOffset NormaliseDateTime(DateTimeOffset dateTime)
{
return dateTime.AddTicks(-(dateTime.Ticks % TimeSpan.TicksPerSecond));
}
}

/// <summary>
Expand Down Expand Up @@ -239,6 +265,11 @@ public sealed record CorrespondenceRequest : MultipartCorrespondenceItem
/// </summary>
public bool? IgnoreReservation { get; init; }

/// <summary>
/// Specifies if reading the correspondence needs to be confirmed by the recipient
/// </summary>
public bool? IsConfirmationNeeded { get; init; }

/// <summary>
/// Existing attachments that should be added to the correspondence
/// </summary>
Expand All @@ -250,32 +281,20 @@ public sealed record CorrespondenceRequest : MultipartCorrespondenceItem
/// <param name="content">The multipart object to serialise into</param>
internal void Serialise(MultipartFormDataContent content)
{
Validate();

AddRequired(content, ResourceId, "Correspondence.ResourceId");
AddRequired(content, Sender.Get(OrganisationNumberFormat.International), "Correspondence.Sender");
AddRequired(content, SendersReference, "Correspondence.SendersReference");
AddRequired(content, AllowSystemDeleteAfter.ToString("O"), "Correspondence.AllowSystemDeleteAfter");
AddRequired(content, AllowSystemDeleteAfter, "Correspondence.AllowSystemDeleteAfter");
AddIfNotNull(content, MessageSender, "Correspondence.MessageSender");
AddIfNotNull(content, RequestedPublishTime?.ToString("O"), "Correspondence.RequestedPublishTime");
AddIfNotNull(content, DueDateTime?.ToString("O"), "Correspondence.DueDateTime");
AddIfNotNull(content, RequestedPublishTime, "Correspondence.RequestedPublishTime");
AddIfNotNull(content, DueDateTime, "Correspondence.DueDateTime");
AddIfNotNull(content, IgnoreReservation?.ToString(), "Correspondence.IgnoreReservation");
AddIfNotNull(content, IsConfirmationNeeded?.ToString(), "Correspondence.IsConfirmationNeeded");
AddDictionaryItems(content, PropertyList, x => x, key => $"Correspondence.PropertyList.{key}");
AddListItems(content, ExistingAttachments, x => x.ToString(), i => $"Correspondence.ExistingAttachments[{i}]");
AddListItems(
content,
Recipients,
x =>
x switch
{
OrganisationOrPersonIdentifier.Organisation org => org.Value.Get(
OrganisationNumberFormat.International
),
OrganisationOrPersonIdentifier.Person person => person.Value,
_ => throw new CorrespondenceValueException(
$"Unknown OrganisationOrPersonIdentifier type `{x.GetType()}` ({nameof(Recipients)})"
),
},
i => $"Recipients[{i}]"
);
AddListItems(content, Recipients, GetFormattedRecipient, i => $"Recipients[{i}]");

Content.Serialise(content);
Notification?.Serialise(content);
Expand All @@ -292,4 +311,53 @@ internal MultipartFormDataContent Serialise()
Serialise(content);
return content;
}

/// <summary>
/// Validates the state of the request based on some known requirements from the Correspondence API
/// </summary>
/// <remarks>
/// Mostly stuff found here: https://github.com/Altinn/altinn-correspondence/blob/main/src/Altinn.Correspondence.Application/InitializeCorrespondences/InitializeCorrespondencesHandler.cs#L51
/// </remarks>
private void Validate()
{
if (Recipients.Count != Recipients.Distinct().Count())
ValidationError($"Duplicate recipients found in {nameof(Recipients)} list");
if (IsConfirmationNeeded is true && DueDateTime is null)
ValidationError($"When {nameof(IsConfirmationNeeded)} is set, {nameof(DueDateTime)} is also required");

var normalisedAllowSystemDeleteAfter = NormaliseDateTime(AllowSystemDeleteAfter);
if (normalisedAllowSystemDeleteAfter < DateTimeOffset.UtcNow)
ValidationError($"{nameof(AllowSystemDeleteAfter)} cannot be a time in the past");
if (normalisedAllowSystemDeleteAfter < RequestedPublishTime)
ValidationError($"{nameof(AllowSystemDeleteAfter)} cannot be prior to {nameof(RequestedPublishTime)}");

if (DueDateTime is not null)
{
var normalisedDueDate = NormaliseDateTime(DueDateTime.Value);
if (normalisedDueDate < DateTimeOffset.UtcNow)
ValidationError($"{nameof(DueDateTime)} cannot be a time in the past");
if (normalisedDueDate < RequestedPublishTime)
ValidationError($"{nameof(DueDateTime)} cannot be prior to {nameof(RequestedPublishTime)}");
if (normalisedAllowSystemDeleteAfter < normalisedDueDate)
ValidationError($"{nameof(AllowSystemDeleteAfter)} cannot be prior to {nameof(DueDateTime)}");
}
}

[DoesNotReturn]
private static void ValidationError(string errorMessage)
{
throw new CorrespondenceArgumentException(errorMessage);
}

private static string GetFormattedRecipient(OrganisationOrPersonIdentifier recipient)
{
return recipient switch
{
OrganisationOrPersonIdentifier.Organisation org => org.Value.Get(OrganisationNumberFormat.International),
OrganisationOrPersonIdentifier.Person person => person.Value.Value,
_ => throw new CorrespondenceValueException(
$"Unknown {nameof(OrganisationOrPersonIdentifier)} type `{recipient.GetType()}`"
),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -278,12 +278,13 @@ private async Task<JwtToken> HandleMaskinportenAltinnTokenExchange(
using HttpResponseMessage response = await client.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();

string token = await response.Content.ReadAsStringAsync(cancellationToken);
string tokenResponse = await response.Content.ReadAsStringAsync(cancellationToken);
JwtToken token = JwtToken.Parse(tokenResponse);

_logger.LogDebug("Token retrieved successfully");
_logger.LogDebug("Token retrieved successfully: {Token}", token);
_telemetry?.RecordMaskinportenAltinnTokenExchangeRequest(Telemetry.Maskinporten.RequestResult.New);

return JwtToken.Parse(token);
return token;
}
catch (MaskinportenException)
{
Expand Down
5 changes: 5 additions & 0 deletions src/Altinn.App.Core/Models/JwtToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ namespace Altinn.App.Core.Models;
public bool IsExpired(TimeProvider? timeProvider = null) =>
ExpiresAt < (timeProvider?.GetUtcNow() ?? DateTimeOffset.UtcNow);

/// <summary>
/// The issuing authority of the token
/// </summary>
public string Issuer => _jwtSecurityToken.Issuer;

/// <summary>
/// The scope(s) associated with the token
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence()
dueDateTime = DateTimeOffset.Now.AddDays(30),
allowDeleteAfter = DateTimeOffset.Now.AddDays(60),
ignoreReservation = true,
isConfirmationNeeded = true,
requestedPublishTime = DateTimeOffset.Now.AddSeconds(45),
propertyList = new Dictionary<string, string> { ["prop1"] = "value1", ["prop2"] = "value2" },
content = new
Expand Down Expand Up @@ -180,6 +181,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence()
.WithDueDateTime(data.dueDateTime)
.WithMessageSender(data.messageSender)
.WithIgnoreReservation(data.ignoreReservation)
.WithIsConfirmationNeeded(data.isConfirmationNeeded)
.WithRequestedPublishTime(data.requestedPublishTime)
.WithPropertyList(data.propertyList)
.WithAttachment(
Expand Down Expand Up @@ -260,6 +262,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence()
correspondence.DueDateTime.Should().Be(data.dueDateTime);
correspondence.AllowSystemDeleteAfter.Should().Be(data.allowDeleteAfter);
correspondence.IgnoreReservation.Should().Be(data.ignoreReservation);
correspondence.IsConfirmationNeeded.Should().Be(data.isConfirmationNeeded);
correspondence.RequestedPublishTime.Should().Be(data.requestedPublishTime);
correspondence.PropertyList.Should().BeEquivalentTo(data.propertyList);
correspondence.MessageSender.Should().Be(data.messageSender);
Expand Down Expand Up @@ -346,7 +349,8 @@ public void Builder_UpdatesAndOverwritesValuesCorrectly()
.WithPropertyList(new Dictionary<string, string> { ["prop1"] = "value1", ["prop2"] = "value2" })
.WithExistingAttachment(Guid.Parse("a3ac4826-5873-4ecb-9fe7-dc4cfccd0afa"))
.WithRequestedPublishTime(DateTime.Today)
.WithIgnoreReservation(true);
.WithIgnoreReservation(true)
.WithIsConfirmationNeeded(true);

builder.WithResourceId("resourceId-2");
builder.WithSender(TestHelpers.GetOrganisationNumber(2).Get(OrganisationNumberFormat.Local));
Expand Down Expand Up @@ -397,6 +401,8 @@ public void Builder_UpdatesAndOverwritesValuesCorrectly()
builder.WithExistingAttachment(Guid.Parse("eeb67483-7d6d-40dc-9861-3fc1beff7608"));
builder.WithExistingAttachments([Guid.Parse("9a12dfd9-6c70-489c-8b3d-77bb188c64b3")]);
builder.WithRequestedPublishTime(DateTime.Today.AddDays(1));
builder.WithIgnoreReservation(false);
builder.WithIsConfirmationNeeded(false);

// Act
var correspondence = builder.Build();
Expand Down Expand Up @@ -487,7 +493,8 @@ public void Builder_UpdatesAndOverwritesValuesCorrectly()
]
);
correspondence.RequestedPublishTime.Should().BeSameDateAs(DateTime.Today.AddDays(1));
correspondence.IgnoreReservation.Should().BeTrue();
correspondence.IgnoreReservation.Should().BeFalse();
correspondence.IsConfirmationNeeded.Should().BeFalse();
}

[Fact]
Expand Down
Loading

0 comments on commit 78cbeb0

Please sign in to comment.