From 30af20a83e430dd2240212762ccdf54d2cee6add Mon Sep 17 00:00:00 2001 From: Phil Schneider Date: Mon, 6 May 2024 13:31:47 +0200 Subject: [PATCH] fix(approval): send mail and notification to requester Refs: https://github.com/eclipse-tractusx/portal-backend/issues/712 --- .../Models/SsiApprovalData.cs | 3 +- .../CompanySsiDetailsRepository.cs | 6 +- .../ICompanySsiDetailsRepository.cs | 2 +- .../BusinessLogic/IssuerBusinessLogic.cs | 37 ++--- .../BusinessLogic/IssuerBusinessLogicTests.cs | 138 ++++++++++++++---- 5 files changed, 134 insertions(+), 52 deletions(-) diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/SsiApprovalData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/SsiApprovalData.cs index 0daf4b1a..31feff78 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Models/SsiApprovalData.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/SsiApprovalData.cs @@ -27,7 +27,8 @@ public record SsiApprovalData( VerifiedCredentialTypeId Type, Guid? ProcessId, VerifiedCredentialTypeKindId? Kind, - string? Bpn, + string Bpn, + string UserId, JsonDocument? Schema, DetailData? DetailData ); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs index 6bbdb837..2f7bee50 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs @@ -199,6 +199,7 @@ public IAsyncEnumerable GetOwnCredentialDetails(str x.ProcessId, x.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind == null ? null : x.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId, x.Bpnl, + x.CreatorUserId, x.CompanySsiProcessData!.Schema, x.VerifiedCredentialExternalTypeDetailVersion == null ? null : @@ -213,13 +214,14 @@ public IAsyncEnumerable GetOwnCredentialDetails(str .SingleOrDefaultAsync(); /// - public Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type, Guid? ProcessId, IEnumerable ProcessStepIds)> GetSsiRejectionData(Guid credentialId) => + public Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type, string UserId, Guid? ProcessId, IEnumerable ProcessStepIds)> GetSsiRejectionData(Guid credentialId) => _context.CompanySsiDetails .Where(x => x.Id == credentialId) - .Select(x => new ValueTuple>( + .Select(x => new ValueTuple>( true, x.CompanySsiDetailStatusId, x.VerifiedCredentialTypeId, + x.CreatorUserId, x.ProcessId, x.Process!.ProcessSteps.Where(ps => ps.ProcessStepStatusId == ProcessStepStatusId.TODO).Select(p => p.Id) )) diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs index 1f96bc84..60ffe823 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs @@ -94,7 +94,7 @@ public interface ICompanySsiDetailsRepository IAsyncEnumerable GetOwnCredentialDetails(string bpnl); Task<(bool exists, SsiApprovalData data)> GetSsiApprovalData(Guid credentialId); - Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type, Guid? ProcessId, IEnumerable ProcessStepIds)> GetSsiRejectionData(Guid credentialId); + Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type, string UserId, Guid? ProcessId, IEnumerable ProcessStepIds)> GetSsiRejectionData(Guid credentialId); void AttachAndModifyCompanySsiDetails(Guid id, Action? initialize, Action updateFields); IAsyncEnumerable GetCertificateTypes(string bpnl); IAsyncEnumerable GetExpiryData(DateTimeOffset now, DateTimeOffset inactiveVcsToDelete, DateTimeOffset expiredVcsToDelete); diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs index 39423d0c..7bcd5f26 100644 --- a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs @@ -215,9 +215,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); } @@ -257,11 +262,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().All(x => x != data.Kind)) @@ -321,7 +321,7 @@ public async Task RejectCredential(Guid credentialId, CancellationToken cancella } var companySsiRepository = _repositories.GetInstance(); - 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()) }); @@ -333,16 +333,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 => { diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/IssuerBusinessLogicTests.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/IssuerBusinessLogicTests.cs index 51d5f562..b1ae5d89 100644 --- a/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/IssuerBusinessLogicTests.cs +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/IssuerBusinessLogicTests.cs @@ -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; @@ -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() - .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(Act); - - // Assert - ex.Message.Should().Be(IssuerErrors.BPN_NOT_SET.ToString()); - A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A._, A>._, A._)).MustNotHaveHappened(); - A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened(); - } - [Fact] public async Task ApproveCredential_WithExpiryInThePast_ReturnsExpected() { @@ -245,6 +224,7 @@ public async Task ApproveCredential_WithExpiryInThePast_ReturnsExpected() null, VerifiedCredentialTypeKindId.FRAMEWORK, Bpnl, + CompanyUserId.ToString(), JsonDocument.Parse(schema), detailData ); @@ -284,6 +264,7 @@ public async Task ApproveCredential_WithInvalidCredentialType_ThrowsException() null, VerifiedCredentialTypeKindId.FRAMEWORK, Bpnl, + CompanyUserId.ToString(), JsonDocument.Parse(schema), useCaseData ); @@ -311,6 +292,7 @@ public async Task ApproveCredential_WithDetailVersionNotSet_ThrowsConflictExcept null, VerifiedCredentialTypeKindId.FRAMEWORK, Bpnl, + CompanyUserId.ToString(), null, null ); @@ -343,6 +325,7 @@ public async Task ApproveCredential_WithAlreadyLinkedProcess_ThrowsConflictExcep Guid.NewGuid(), VerifiedCredentialTypeKindId.FRAMEWORK, Bpnl, + CompanyUserId.ToString(), null, new DetailData( VerifiedCredentialExternalTypeId.TRACEABILITY_CREDENTIAL, @@ -369,16 +352,15 @@ 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 @@ -386,15 +368,16 @@ public async Task ApproveCredential_WithValid_ReturnsExpected(VerifiedCredential 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)); @@ -426,6 +409,63 @@ public async Task ApproveCredential_WithValid_ReturnsExpected(VerifiedCredential processData.Schema.Deserialize()!.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?>._, A>._!)) + .Invokes((Guid _, Action? initialize, Action updateFields) => + { + initialize?.Invoke(detail); + updateFields.Invoke(detail); + }); + A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyProcessData(CredentialId, A?>._, A>._!)) + .Invokes((Guid _, Action? initialize, Action updateFields) => + { + initialize?.Invoke(processData); + updateFields.Invoke(processData); + }); + + // Act + await _sut.ApproveCredential(CredentialId, CancellationToken.None); + + // Assert + A.CallTo(() => _portalService.AddNotification(A._, A._, NotificationTypeId.CREDENTIAL_APPROVAL, A._)).MustNotHaveHappened(); + A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A._, A>._, A._)).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()!.IssuanceDate.Should().Be(now); + } + private static string CreateSchema() { var schemaData = new FrameworkCredential( @@ -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))); + .Returns(default((bool, CompanySsiDetailStatusId, VerifiedCredentialTypeId, string, Guid?, IEnumerable))); Task Act() => _sut.RejectCredential(notExistingId, CancellationToken.None); // Act @@ -488,6 +528,7 @@ public async Task RejectCredential_WithNotPendingSsiDetail_ThrowsNotFoundExcepti true, status, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, + CompanyUserId.ToString(), null, Enumerable.Empty() )); @@ -514,6 +555,7 @@ public async Task RejectCredential_WithValidRequest_ReturnsExpected() true, CompanySsiDetailStatusId.PENDING, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, + CompanyUserId.ToString(), null, Enumerable.Empty())); A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(CredentialId, A?>._, A>._!)) @@ -547,6 +589,7 @@ public async Task RejectCredential_WithValidRequestAndPendingProcessStepIds_Retu true, CompanySsiDetailStatusId.PENDING, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, + CompanyUserId.ToString(), Guid.NewGuid(), Enumerable.Repeat(Guid.NewGuid(), 1))); A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(CredentialId, A?>._, A>._!)) @@ -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?>._, A>._!)) + .Invokes((Guid _, Action? initialize, Action updateFields) => + { + initialize?.Invoke(detail); + updateFields.Invoke(detail); + }); + + // Act + await _sut.RejectCredential(CredentialId, CancellationToken.None); + + // Assert + A.CallTo(() => _portalService.TriggerMail(A._, A._, A>._, A._)).MustNotHaveHappened(); + A.CallTo(() => _portalService.AddNotification(A._, A._, A._, A._)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A? Initialize, Action Modify)>>._)).MustHaveHappenedOnceExactly(); + + detail.CompanySsiDetailStatusId.Should().Be(CompanySsiDetailStatusId.INACTIVE); + detail.DateLastChanged.Should().Be(now); + } + #endregion #region GetCertificateTypes