Skip to content

Commit

Permalink
fix(approval): send mail and notification to requester (#101)
Browse files Browse the repository at this point in the history
Refs: eclipse-tractusx/portal-backend#712
Reviewed-By: Norbert Truchsess <[email protected]>
  • Loading branch information
Phil91 authored May 12, 2024
1 parent 7e8df9d commit 0fe249c
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public record SsiApprovalData(
VerifiedCredentialTypeId Type,
Guid? ProcessId,
VerifiedCredentialTypeKindId? Kind,
string? Bpn,
string Bpn,
string UserId,
JsonDocument? Schema,
DetailData? DetailData
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ public IAsyncEnumerable<OwnedVerifiedCredentialData> GetOwnCredentialDetails(str
x.ProcessId,
x.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind == null ? null : x.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId,
x.Bpnl,
x.CreatorUserId,
x.CompanySsiProcessData!.Schema,
x.VerifiedCredentialExternalTypeDetailVersion == null ?
null :
Expand All @@ -214,13 +215,14 @@ public IAsyncEnumerable<OwnedVerifiedCredentialData> GetOwnCredentialDetails(str
.SingleOrDefaultAsync();

/// <inheritdoc />
public Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type, Guid? ProcessId, IEnumerable<Guid> ProcessStepIds)> GetSsiRejectionData(Guid credentialId) =>
public Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type, string UserId, Guid? ProcessId, IEnumerable<Guid> ProcessStepIds)> GetSsiRejectionData(Guid credentialId) =>
_context.CompanySsiDetails
.Where(x => x.Id == credentialId)
.Select(x => new ValueTuple<bool, CompanySsiDetailStatusId, VerifiedCredentialTypeId, Guid?, IEnumerable<Guid>>(
.Select(x => new ValueTuple<bool, CompanySsiDetailStatusId, VerifiedCredentialTypeId, string, Guid?, IEnumerable<Guid>>(
true,
x.CompanySsiDetailStatusId,
x.VerifiedCredentialTypeId,
x.CreatorUserId,
x.ProcessId,
x.Process!.ProcessSteps.Where(ps => ps.ProcessStepStatusId == ProcessStepStatusId.TODO).Select(p => p.Id)
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public interface ICompanySsiDetailsRepository
IAsyncEnumerable<OwnedVerifiedCredentialData> GetOwnCredentialDetails(string bpnl);

Task<(bool exists, SsiApprovalData data)> GetSsiApprovalData(Guid credentialId);
Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type, Guid? ProcessId, IEnumerable<Guid> ProcessStepIds)> GetSsiRejectionData(Guid credentialId);
Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type, string UserId, Guid? ProcessId, IEnumerable<Guid> ProcessStepIds)> GetSsiRejectionData(Guid credentialId);
void AttachAndModifyCompanySsiDetails(Guid id, Action<CompanySsiDetail>? initialize, Action<CompanySsiDetail> updateFields);
IAsyncEnumerable<VerifiedCredentialTypeId> GetCertificateTypes(string bpnl);
IAsyncEnumerable<CredentialExpiryData> GetExpiryData(DateTimeOffset now, DateTimeOffset inactiveVcsToDelete, DateTimeOffset expiredVcsToDelete);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,14 @@ public async Task ApproveCredential(Guid credentialId, CancellationToken cancell
new("credentialType", typeValue),
new("expiryDate", expiry.ToString("o", CultureInfo.InvariantCulture))
};
await _portalService.TriggerMail("CredentialApproval", _identity.CompanyUserId.Value, mailParameters, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
var content = JsonSerializer.Serialize(new { data.Type, CredentialId = credentialId }, Options);
await _portalService.AddNotification(content, _identity.CompanyUserId.Value, NotificationTypeId.CREDENTIAL_APPROVAL, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);

if (Guid.TryParse(data.UserId, out var companyUserId))
{
await _portalService.TriggerMail("CredentialApproval", companyUserId, mailParameters, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
var content = JsonSerializer.Serialize(new { data.Type, CredentialId = credentialId }, Options);
await _portalService.AddNotification(content, companyUserId, NotificationTypeId.CREDENTIAL_APPROVAL, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
}

await _repositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
}

Expand Down Expand Up @@ -248,11 +253,6 @@ private static void ValidateApprovalData(Guid credentialId, bool exists, SsiAppr
throw ConflictException.Create(IssuerErrors.CREDENTIAL_NOT_PENDING, new ErrorParameter[] { new("credentialId", credentialId.ToString()), new("status", CompanySsiDetailStatusId.PENDING.ToString()) });
}

if (string.IsNullOrWhiteSpace(data.Bpn))
{
throw UnexpectedConditionException.Create(IssuerErrors.BPN_NOT_SET);
}

ValidateFrameworkCredential(data);

if (Enum.GetValues<VerifiedCredentialTypeKindId>().All(x => x != data.Kind))
Expand Down Expand Up @@ -312,7 +312,7 @@ public async Task RejectCredential(Guid credentialId, CancellationToken cancella
}

var companySsiRepository = _repositories.GetInstance<ICompanySsiDetailsRepository>();
var (exists, status, type, processId, processStepIds) = await companySsiRepository.GetSsiRejectionData(credentialId).ConfigureAwait(ConfigureAwaitOptions.None);
var (exists, status, type, userId, processId, processStepIds) = await companySsiRepository.GetSsiRejectionData(credentialId).ConfigureAwait(ConfigureAwaitOptions.None);
if (!exists)
{
throw NotFoundException.Create(IssuerErrors.SSI_DETAILS_NOT_FOUND, new ErrorParameter[] { new("credentialId", credentialId.ToString()) });
Expand All @@ -324,16 +324,17 @@ public async Task RejectCredential(Guid credentialId, CancellationToken cancella
}

var typeValue = type.GetEnumValue() ?? throw UnexpectedConditionException.Create(IssuerErrors.CREDENTIAL_TYPE_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialType", type.ToString()) });
var content = JsonSerializer.Serialize(new { Type = type, CredentialId = credentialId }, Options);
await _portalService.AddNotification(content, _identity.CompanyUserId.Value, NotificationTypeId.CREDENTIAL_REJECTED, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);

var mailParameters = new MailParameter[]
if (Guid.TryParse(userId, out var companyUserId))
{
new("requestName", typeValue),
new("reason", "Declined by the Operator")
};

await _portalService.TriggerMail("CredentialRejected", _identity.CompanyUserId.Value, mailParameters, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
var content = JsonSerializer.Serialize(new { Type = type, CredentialId = credentialId }, Options);
await _portalService.AddNotification(content, companyUserId, NotificationTypeId.CREDENTIAL_REJECTED, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
var mailParameters = new MailParameter[]
{
new("requestName", typeValue),
new("reason", "Declined by the Operator")
};
await _portalService.TriggerMail("CredentialRejected", companyUserId, mailParameters, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
}

companySsiRepository.AttachAndModifyCompanySsiDetails(credentialId, c =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class IssuerBusinessLogicTests
private static readonly Guid CredentialId = Guid.NewGuid();
private static readonly string Bpnl = "BPNL00000001TEST";
private static readonly string IssuerBpnl = "BPNL000001ISSUER";
private static readonly Guid CompanyUserId = Guid.NewGuid();

private readonly IFixture _fixture;
private readonly ICompanySsiDetailsRepository _companySsiDetailsRepository;
Expand Down Expand Up @@ -203,28 +204,6 @@ public async Task ApproveCredential_WithStatusNotPending_ThrowsConflictException
A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened();
}

[Fact]
public async Task ApproveCredential_WithBpnNotSetActiveSsiDetail_ThrowsConflictException()
{
// Arrange
var alreadyActiveId = Guid.NewGuid();
var approvalData = _fixture.Build<SsiApprovalData>()
.With(x => x.Status, CompanySsiDetailStatusId.PENDING)
.With(x => x.Bpn, (string?)null)
.Create();
A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(alreadyActiveId))
.Returns((true, approvalData));
Task Act() => _sut.ApproveCredential(alreadyActiveId, CancellationToken.None);

// Act
var ex = await Assert.ThrowsAsync<UnexpectedConditionException>(Act);

// Assert
ex.Message.Should().Be(IssuerErrors.BPN_NOT_SET.ToString());
A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A<Guid>._, A<IEnumerable<MailParameter>>._, A<CancellationToken>._)).MustNotHaveHappened();
A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened();
}

[Fact]
public async Task ApproveCredential_WithExpiryInThePast_ReturnsExpected()
{
Expand All @@ -245,6 +224,7 @@ public async Task ApproveCredential_WithExpiryInThePast_ReturnsExpected()
null,
VerifiedCredentialTypeKindId.FRAMEWORK,
Bpnl,
CompanyUserId.ToString(),
JsonDocument.Parse(schema),
detailData
);
Expand Down Expand Up @@ -284,6 +264,7 @@ public async Task ApproveCredential_WithInvalidCredentialType_ThrowsException()
null,
VerifiedCredentialTypeKindId.FRAMEWORK,
Bpnl,
CompanyUserId.ToString(),
JsonDocument.Parse(schema),
useCaseData
);
Expand Down Expand Up @@ -311,6 +292,7 @@ public async Task ApproveCredential_WithDetailVersionNotSet_ThrowsConflictExcept
null,
VerifiedCredentialTypeKindId.FRAMEWORK,
Bpnl,
CompanyUserId.ToString(),
null,
null
);
Expand Down Expand Up @@ -343,6 +325,7 @@ public async Task ApproveCredential_WithAlreadyLinkedProcess_ThrowsConflictExcep
Guid.NewGuid(),
VerifiedCredentialTypeKindId.FRAMEWORK,
Bpnl,
CompanyUserId.ToString(),
null,
new DetailData(
VerifiedCredentialExternalTypeId.TRACEABILITY_CREDENTIAL,
Expand All @@ -369,32 +352,32 @@ public async Task ApproveCredential_WithAlreadyLinkedProcess_ThrowsConflictExcep
ex.Message.Should().Be(IssuerErrors.ALREADY_LINKED_PROCESS.ToString());
}

[Theory]
[InlineData(VerifiedCredentialTypeKindId.FRAMEWORK, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, VerifiedCredentialExternalTypeId.TRACEABILITY_CREDENTIAL)]
public async Task ApproveCredential_WithValid_ReturnsExpected(VerifiedCredentialTypeKindId kindId, VerifiedCredentialTypeId typeId, VerifiedCredentialExternalTypeId externalTypeId)
[Fact]
public async Task ApproveCredential_WithValid_ReturnsExpected()
{
// Arrange
var schema = CreateSchema();
var processData = new CompanySsiProcessData(CredentialId, JsonDocument.Parse(schema), VerifiedCredentialTypeKindId.FRAMEWORK);
var now = DateTimeOffset.UtcNow;
var detailData = new DetailData(
externalTypeId,
VerifiedCredentialExternalTypeId.TRACEABILITY_CREDENTIAL,
"test",
"1.0.0",
DateTimeOffset.UtcNow
);

var data = new SsiApprovalData(
CompanySsiDetailStatusId.PENDING,
typeId,
VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK,
null,
kindId,
VerifiedCredentialTypeKindId.FRAMEWORK,
Bpnl,
CompanyUserId.ToString(),
JsonDocument.Parse(schema),
detailData
);

var detail = new CompanySsiDetail(CredentialId, _identity.Bpnl, typeId, CompanySsiDetailStatusId.PENDING, "", Guid.NewGuid().ToString(), DateTimeOffset.Now);
var detail = new CompanySsiDetail(CredentialId, _identity.Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, CompanySsiDetailStatusId.PENDING, "", Guid.NewGuid().ToString(), DateTimeOffset.Now);
A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now);
A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(CredentialId))
.Returns((true, data));
Expand Down Expand Up @@ -426,6 +409,63 @@ public async Task ApproveCredential_WithValid_ReturnsExpected(VerifiedCredential
processData.Schema.Deserialize<FrameworkCredential>()!.IssuanceDate.Should().Be(now);
}

[Fact]
public async Task ApproveCredential_WithValidWithoutCompanyUserRequester_DoesNotSendMailAndNotification()
{
// Arrange
var schema = CreateSchema();
var processData = new CompanySsiProcessData(CredentialId, JsonDocument.Parse(schema), VerifiedCredentialTypeKindId.FRAMEWORK);
var now = DateTimeOffset.UtcNow;
var detailData = new DetailData(
VerifiedCredentialExternalTypeId.TRACEABILITY_CREDENTIAL,
"test",
"1.0.0",
DateTimeOffset.UtcNow
);

var data = new SsiApprovalData(
CompanySsiDetailStatusId.PENDING,
VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK,
null,
VerifiedCredentialTypeKindId.FRAMEWORK,
Bpnl,
"test123",
JsonDocument.Parse(schema),
detailData
);

var detail = new CompanySsiDetail(CredentialId, _identity.Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, CompanySsiDetailStatusId.PENDING, "", "test123", DateTimeOffset.Now);
A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now);
A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(CredentialId))
.Returns((true, data));
A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(CredentialId, A<Action<CompanySsiDetail>?>._, A<Action<CompanySsiDetail>>._!))
.Invokes((Guid _, Action<CompanySsiDetail>? initialize, Action<CompanySsiDetail> updateFields) =>
{
initialize?.Invoke(detail);
updateFields.Invoke(detail);
});
A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyProcessData(CredentialId, A<Action<CompanySsiProcessData>?>._, A<Action<CompanySsiProcessData>>._!))
.Invokes((Guid _, Action<CompanySsiProcessData>? initialize, Action<CompanySsiProcessData> updateFields) =>
{
initialize?.Invoke(processData);
updateFields.Invoke(processData);
});

// Act
await _sut.ApproveCredential(CredentialId, CancellationToken.None);

// Assert
A.CallTo(() => _portalService.AddNotification(A<string>._, A<Guid>._, NotificationTypeId.CREDENTIAL_APPROVAL, A<CancellationToken>._)).MustNotHaveHappened();
A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A<Guid>._, A<IEnumerable<MailParameter>>._, A<CancellationToken>._)).MustNotHaveHappened();
A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly();
A.CallTo(() => _processStepRepository.CreateProcess(ProcessTypeId.CREATE_CREDENTIAL))
.MustHaveHappenedOnceExactly();

detail.CompanySsiDetailStatusId.Should().Be(CompanySsiDetailStatusId.ACTIVE);
detail.DateLastChanged.Should().Be(now);
processData.Schema.Deserialize<FrameworkCredential>()!.IssuanceDate.Should().Be(now);
}

private static string CreateSchema()
{
var schemaData = new FrameworkCredential(
Expand Down Expand Up @@ -464,7 +504,7 @@ public async Task RejectCredential_WithoutExistingSsiDetail_ThrowsNotFoundExcept
// Arrange
var notExistingId = Guid.NewGuid();
A.CallTo(() => _companySsiDetailsRepository.GetSsiRejectionData(notExistingId))
.Returns(default((bool, CompanySsiDetailStatusId, VerifiedCredentialTypeId, Guid?, IEnumerable<Guid>)));
.Returns(default((bool, CompanySsiDetailStatusId, VerifiedCredentialTypeId, string, Guid?, IEnumerable<Guid>)));
Task Act() => _sut.RejectCredential(notExistingId, CancellationToken.None);

// Act
Expand All @@ -488,6 +528,7 @@ public async Task RejectCredential_WithNotPendingSsiDetail_ThrowsNotFoundExcepti
true,
status,
VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK,
CompanyUserId.ToString(),
null,
Enumerable.Empty<Guid>()
));
Expand All @@ -514,6 +555,7 @@ public async Task RejectCredential_WithValidRequest_ReturnsExpected()
true,
CompanySsiDetailStatusId.PENDING,
VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK,
CompanyUserId.ToString(),
null,
Enumerable.Empty<Guid>()));
A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(CredentialId, A<Action<CompanySsiDetail>?>._, A<Action<CompanySsiDetail>>._!))
Expand Down Expand Up @@ -547,6 +589,7 @@ public async Task RejectCredential_WithValidRequestAndPendingProcessStepIds_Retu
true,
CompanySsiDetailStatusId.PENDING,
VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK,
CompanyUserId.ToString(),
Guid.NewGuid(),
Enumerable.Repeat<Guid>(Guid.NewGuid(), 1)));
A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(CredentialId, A<Action<CompanySsiDetail>?>._, A<Action<CompanySsiDetail>>._!))
Expand All @@ -569,6 +612,41 @@ public async Task RejectCredential_WithValidRequestAndPendingProcessStepIds_Retu
detail.DateLastChanged.Should().Be(now);
}

[Fact]
public async Task RejectCredential_WithValidWithoutCompanyUserRequester_DoesNotSendMailAndNotification()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var detail = new CompanySsiDetail(CredentialId, _identity.Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, CompanySsiDetailStatusId.PENDING, IssuerBpnl, "test123", DateTimeOffset.Now);
A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now);
A.CallTo(() => _companySsiDetailsRepository.GetSsiRejectionData(CredentialId))
.Returns((
true,
CompanySsiDetailStatusId.PENDING,
VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK,
"test123",
Guid.NewGuid(),
Enumerable.Repeat(Guid.NewGuid(), 1)));
A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(CredentialId, A<Action<CompanySsiDetail>?>._, A<Action<CompanySsiDetail>>._!))
.Invokes((Guid _, Action<CompanySsiDetail>? initialize, Action<CompanySsiDetail> updateFields) =>
{
initialize?.Invoke(detail);
updateFields.Invoke(detail);
});

// Act
await _sut.RejectCredential(CredentialId, CancellationToken.None);

// Assert
A.CallTo(() => _portalService.TriggerMail(A<string>._, A<Guid>._, A<IEnumerable<MailParameter>>._, A<CancellationToken>._)).MustNotHaveHappened();
A.CallTo(() => _portalService.AddNotification(A<string>._, A<Guid>._, A<NotificationTypeId>._, A<CancellationToken>._)).MustNotHaveHappened();
A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly();
A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A<IEnumerable<(Guid ProcessStepId, Action<ProcessStep>? Initialize, Action<ProcessStep> Modify)>>._)).MustHaveHappenedOnceExactly();

detail.CompanySsiDetailStatusId.Should().Be(CompanySsiDetailStatusId.INACTIVE);
detail.DateLastChanged.Should().Be(now);
}

#endregion

#region GetCertificateTypes
Expand Down

0 comments on commit 0fe249c

Please sign in to comment.