Skip to content

Commit

Permalink
Consumer API: Revoke a Relationship reactivation request (#600)
Browse files Browse the repository at this point in the history
* chore: move entities to Aggregates folder

* feat: creation of Relationship and remove Changes related stuff

* test: split into two tests

* feat: acceptance of creation

* feat: reject creation

* feat: revoke creation

* test: split relationship tests into multiple files

* feat: allow multiple relationships as long as there is only one active

* chore: remove redundant parameter

* refactor: make RelationshipTemplatesRepository.Find return null instead of throwing

* feat: add Handler

* feat: add and use expressions

* chore: don't use AutoMapper and add more tests

* feat: reject relationship

* feat: AcceptRelationshipCommand

* feat: RevokeRelationshipCommand

* feat: add AuditLog to DTOs

* feat: add CreationContent property to RelationshipDTO

* feat: add additional properties to RelationshipCreatedIntegrationEvent and RelationshipStatusChangedIntegrationEvent

* feat: handle new integration events in Synchronization module

* chore: formatting

* test: fix tests

* feat: replace integration events in quotas module with new ones

* feat: add migration

* feat: add controller methods

* chore: fix/ignore compiler warnings

* refactor: cleanup error codes

* feat: add insomnia workspace

* feat: add openapi.yml

* fix: add RelationshipMetadataDTO type and add creationContent property to RelationshipDTO

* refactor: rename Content to CreationContent in request to create a relationship

* chore: update InsomniaWorkspace and openapi.yml

* chore: rename RelationshipStatus "Accepted" to "Active"

* chore: fix merge conflicts

* feat: implement domain part

* feat: implement application part

* feat: implement controller

* chore: remove redundant whitespace

* test: add domain and handler tests

* feat: update domain errors

* feat: trigger external event

* chore: fix formatting

* chore: update files prior to making PR ready for review

* chore: fix formatting

* feat: add AcceptanceContent

* fix: avoid error on creation of RelationshipsOverview view when RelationshipChanges table does not exist

* feat: (WIP!!): update Admin API RelationshipOverviews view

* feat: add AcceptanceContent to DTO

* fix: pass AcceptanceContent to AcceptRelationshipCommand

* chore: use postgres in Admin CLI launchSettings.json

* chore: fix formatting

* chore: add _relationshipTemplateAllocations field

* fix: update failing test

* fix: update tests

* fix: relationships overview migration

* feat: update revamped relationships overview view with audit log

* feat: implementing PR change requests

* feat: implement PR change requests

* chore: remove CreatedAt property

* feat: implement PR change requests

* chore: remove redundant Relationship statuses

* fix: update condition

* chore: update identifier

* Merge branch 'release/v5' of github.com:nmshd/backbone into nmshdb-89-termination-of-a-relationship

* chore: fix formatting

* chore: update object property name

* Merge branch 'release/v5' of github.com:nmshd/backbone into nmshdb-93-revocation-of-a-reactivation-of-a-relationship

* feat: implement PR change requests

* feat: order audit logs before accessing the last

* fix: return helper method

* chore: fix formatting

* fix: consider the time of the reactivation within the given quota period

* fix: remove unused relationship created type

* feat: address PR change requests

* feat: address PR change requests

* chore: add named parameters to test methods

* chore: fix formatting

* fix: remove nullable reference type from CreatedBy prop, RelationshipAuditLogEntry

---------

Co-authored-by: Timo Notheisen <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Timo Notheisen <[email protected]>
Co-authored-by: Daniel Almeida <[email protected]>
Co-authored-by: Hunor Tot-Bagi <[email protected]>
Co-authored-by: Nikola Dmitrasinovic <[email protected]>
  • Loading branch information
7 people authored May 14, 2024
1 parent 13cae23 commit 6f90d49
Show file tree
Hide file tree
Showing 32 changed files with 402 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ public static IQueryable<Relationship> WithParticipants(this IQueryable<Relation
{
return query.Where(r => r.From == participant1 && r.To == participant2 || r.From == participant2 && r.To == participant1);
}

public static IQueryable<Relationship> Active(this IQueryable<Relationship> query)
{
return query.Where(r => r.Status == RelationshipStatus.Active);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ private RelationshipAuditLogEntry()
{
// This constructor is for EF Core only; initializing the properties with null is therefore not a problem
Id = null!;
CreatedBy = null!;
}

public string Id { get; set; }
public RelationshipAuditLogEntryReason Reason { get; set; }
public DateTime CreatedAt { get; set; }
public string? CreatedBy { get; internal set; }
public string CreatedBy { get; set; }
}

public enum RelationshipAuditLogEntryReason
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ public async Task<uint> Count(string createdBy, DateTime createdAtFrom, DateTime
.Include(r => r.AuditLog)
.CreatedInInterval(createdAtFrom, createdAtTo)
.Where(r => r.Status == RelationshipStatus.Pending && r.From == createdBy ||
r.Status == RelationshipStatus.Active && (r.From == createdBy || r.To == createdBy) ||
r.Status == RelationshipStatus.Terminated &&
r.AuditLog.OrderBy(a => a.CreatedAt).Last().Reason == RelationshipAuditLogEntryReason.ReactivationRequested &&
r.AuditLog.OrderBy(a => a.CreatedAt).Last().CreatedBy == createdBy)
r.Status == RelationshipStatus.Active && (r.From == createdBy || r.To == createdBy) ||
r.Status == RelationshipStatus.Terminated &&
r.AuditLog
.OrderByDescending(a => a.CreatedAt)
.FirstOrDefault(a =>
a.Reason == RelationshipAuditLogEntryReason.ReactivationRequested &&
a.CreatedBy == createdBy &&
a.CreatedAt > createdAtFrom &&
a.CreatedAt < createdAtTo) != null)
.CountAsync(cancellationToken);
return (uint)relationshipsCount;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,28 @@ public async Task Relationship_with_requested_reactivation_only_counts_for_for_r
countForI1.Should().Be(1);
countForI2.Should().Be(1);
}

[Fact]
public async Task Requested_reactivation_outside_given_quota_period_does_not_count_for_any_participant()
{
// Arrange
var relationships = new List<Relationship>
{
CreateRelationshipWithRequestedReactivation(from: I1, to: I2, reactivationRequestedBy: I1),
CreateRelationshipWithRequestedReactivation(from: I2, to: I1, reactivationRequestedBy: I2)
};
await _relationshipsArrangeContext.Relationships.AddRangeAsync(relationships);
await _relationshipsArrangeContext.SaveChangesAsync();

var repository = new RelationshipsRepository(_actContext);
const QuotaPeriod quotaPeriod = QuotaPeriod.Hour;

// Act
var countForI1 = await repository.Count(I1, quotaPeriod.CalculateBegin().AddHours(2), quotaPeriod.CalculateEnd().AddHours(2), CancellationToken.None);
var countForI2 = await repository.Count(I2, quotaPeriod.CalculateBegin().AddHours(2), quotaPeriod.CalculateEnd().AddHours(2), CancellationToken.None);

// Assert
countForI1.Should().Be(0);
countForI2.Should().Be(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Relationships.Domain.Aggregates.Relationships;
using Backbone.Modules.Relationships.Domain.DomainEvents.Outgoing;
using MediatR;

namespace Backbone.Modules.Relationships.Application.Relationships.Commands.RevokeRelationshipReactivation;
public class Handler : IRequestHandler<RevokeRelationshipReactivationCommand, RevokeRelationshipReactivationResponse>
{
private readonly IRelationshipsRepository _relationshipsRepository;
private readonly IEventBus _eventBus;
private readonly IdentityAddress _activeIdentity;
private readonly DeviceId _activeDevice;

public Handler(IRelationshipsRepository relationshipsRepository, IUserContext userContext, IEventBus eventBus)
{
_relationshipsRepository = relationshipsRepository;
_eventBus = eventBus;
_activeIdentity = userContext.GetAddress();
_activeDevice = userContext.GetDeviceId();
}

public async Task<RevokeRelationshipReactivationResponse> Handle(RevokeRelationshipReactivationCommand request, CancellationToken cancellationToken)
{
var relationshipId = RelationshipId.Parse(request.RelationshipId);
var relationship = await _relationshipsRepository.FindRelationship(relationshipId, _activeIdentity, cancellationToken, track: true);

relationship.RevokeReactivation(_activeIdentity, _activeDevice);

await _relationshipsRepository.Update(relationship);

var peer = relationship.To == _activeIdentity ? relationship.From : relationship.To;

_eventBus.Publish(new RelationshipReactivationCompletedDomainEvent(relationship, peer));

return new RevokeRelationshipReactivationResponse(relationship);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MediatR;

namespace Backbone.Modules.Relationships.Application.Relationships.Commands.RevokeRelationshipReactivation;
public class RevokeRelationshipReactivationCommand : IRequest<RevokeRelationshipReactivationResponse>
{
public required string RelationshipId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Backbone.Modules.Relationships.Application.Relationships.DTOs;
using Backbone.Modules.Relationships.Domain.Aggregates.Relationships;

namespace Backbone.Modules.Relationships.Application.Relationships.Commands.RevokeRelationshipReactivation;
public class RevokeRelationshipReactivationResponse : RelationshipMetadataDTO
{
public RevokeRelationshipReactivationResponse(Relationship relationship) : base(relationship) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Backbone.Modules.Relationships.Application.Relationships.Commands.RejectRelationship;
using Backbone.Modules.Relationships.Application.Relationships.Commands.RelationshipReactivationRequest;
using Backbone.Modules.Relationships.Application.Relationships.Commands.RevokeRelationship;
using Backbone.Modules.Relationships.Application.Relationships.Commands.RevokeRelationshipReactivation;
using Backbone.Modules.Relationships.Application.Relationships.Commands.TerminateRelationship;
using Backbone.Modules.Relationships.Application.Relationships.DTOs;
using Backbone.Modules.Relationships.Application.Relationships.Queries.GetRelationship;
Expand Down Expand Up @@ -100,6 +101,16 @@ public async Task<IActionResult> RevokeRelationship([FromRoute] string id, [From
return Ok(response);
}

[HttpPut("{id}/Reactivate/Revoke")]
[ProducesResponseType(typeof(HttpResponseEnvelopeResult<RevokeRelationshipReactivationResponse>), StatusCodes.Status200OK)]
[ProducesError(StatusCodes.Status400BadRequest)]
[ProducesError(StatusCodes.Status404NotFound)]
public async Task<IActionResult> RevokeRelationshipReactivation([FromRoute] string id, CancellationToken cancellationToken)
{
var response = await _mediator.Send(new RevokeRelationshipReactivationCommand { RelationshipId = id }, cancellationToken);
return Ok(response);
}

[HttpPut("{id}/Terminate")]
[ProducesResponseType(typeof(HttpResponseEnvelopeResult<RelationshipDTO>), StatusCodes.Status200OK)]
[ProducesError(StatusCodes.Status400BadRequest)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public Relationship(RelationshipTemplate relationshipTemplate, IdentityAddress a
public byte[]? CreationResponseContent { get; private set; }
public List<RelationshipAuditLogEntry> AuditLog { get; }

public IdentityAddress LastModifiedBy => AuditLog.Last().CreatedBy;
public IdentityAddress LastModifiedBy => AuditLog.OrderBy(a => a.CreatedAt).Last().CreatedBy;

private static void EnsureTargetIsNotSelf(RelationshipTemplate relationshipTemplate, IdentityAddress activeIdentity)
{
Expand Down Expand Up @@ -89,16 +89,16 @@ public void Accept(IdentityAddress activeIdentity, DeviceId activeDevice, byte[]
AuditLog.Add(auditLogEntry);
}

private void EnsureRelationshipRequestIsAddressedToSelf(IdentityAddress activeIdentity)
private void EnsureStatus(RelationshipStatus status)
{
if (To != activeIdentity)
throw new DomainException(DomainErrors.CannotAcceptOrRejectRelationshipRequestAddressedToSomeoneElse());
if (Status != status)
throw new DomainException(DomainErrors.RelationshipIsNotInCorrectStatus(status));
}

private void EnsureRelationshipRequestIsCreatedBySelf(IdentityAddress activeIdentity)
private void EnsureRelationshipRequestIsAddressedToSelf(IdentityAddress activeIdentity)
{
if (From != activeIdentity)
throw new DomainException(DomainErrors.CannotRevokeRelationshipRequestNotCreatedByYourself());
if (To != activeIdentity)
throw new DomainException(DomainErrors.CannotAcceptOrRejectRelationshipRequestAddressedToSomeoneElse());
}

public void Reject(IdentityAddress activeIdentity, DeviceId activeDevice, byte[]? creationResponseContent)
Expand All @@ -119,12 +119,6 @@ public void Reject(IdentityAddress activeIdentity, DeviceId activeDevice, byte[]
AuditLog.Add(auditLogEntry);
}

private void EnsureStatus(RelationshipStatus status)
{
if (Status != status)
throw new DomainException(DomainErrors.RelationshipIsNotInCorrectStatus(status));
}

public void Revoke(IdentityAddress activeIdentity, DeviceId activeDevice, byte[]? creationResponseContent)
{
EnsureStatus(RelationshipStatus.Pending);
Expand All @@ -143,6 +137,12 @@ public void Revoke(IdentityAddress activeIdentity, DeviceId activeDevice, byte[]
AuditLog.Add(auditLogEntry);
}

private void EnsureRelationshipRequestIsCreatedBySelf(IdentityAddress activeIdentity)
{
if (From != activeIdentity)
throw new DomainException(DomainErrors.CannotRevokeRelationshipRequestNotCreatedByYourself());
}

public void Terminate(IdentityAddress activeIdentity, DeviceId activeDevice)
{
EnsureStatus(RelationshipStatus.Active);
Expand All @@ -162,7 +162,6 @@ public void Terminate(IdentityAddress activeIdentity, DeviceId activeDevice)
public void RequestReactivation(IdentityAddress activeIdentity, DeviceId activeDevice)
{
EnsureThereIsNoOpenReactivationRequest();

EnsureStatus(RelationshipStatus.Terminated);

var auditLogEntry = new RelationshipAuditLogEntry(
Expand All @@ -183,6 +182,27 @@ private void EnsureThereIsNoOpenReactivationRequest()
throw new DomainException(DomainErrors.CannotRequestReactivationWhenThereIsAnOpenReactivationRequest());
}

public void RevokeReactivation(IdentityAddress activeIdentity, DeviceId activeDevice)
{
EnsureRevocableReactivationRequestExistsFor(activeIdentity);

var auditLogEntry = new RelationshipAuditLogEntry(
RelationshipAuditLogEntryReason.RevocationOfReactivation,
RelationshipStatus.Terminated,
RelationshipStatus.Terminated,
activeIdentity,
activeDevice
);
AuditLog.Add(auditLogEntry);
}

private void EnsureRevocableReactivationRequestExistsFor(IdentityAddress activeIdentity)
{
if (AuditLog.OrderBy(a => a.CreatedAt).Last().Reason != RelationshipAuditLogEntryReason.ReactivationRequested ||
AuditLog.OrderBy(a => a.CreatedAt).Last().CreatedBy != activeIdentity)
throw new DomainException(DomainErrors.NoRevocableReactivationRequestExists(activeIdentity));
}

#region Expressions

public static Expression<Func<Relationship, bool>> HasParticipant(string identity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ public enum RelationshipAuditLogEntryReason
RejectionOfCreation = 2,
RevocationOfCreation = 3,
Termination = 4,
ReactivationRequested = 5
ReactivationRequested = 5,
AcceptanceOfReactivation = 6,
RejectionOfReactivation = 7,
RevocationOfReactivation = 8
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public static DomainError RelationshipToTargetAlreadyExists(string targetIdentit
$"A relationship to '{targetIdentity}' already exists. If the relationship is terminated, you can reactivate it.");
}

public static DomainError NoRevocableReactivationRequestExists(string activeIdentity)
{
return new DomainError("error.platform.validation.relationshipRequest.noRevocableReactivationRequestExists",
$"There is no pending reactivation request or you are not allowed to revoke it. A reactivation request can only be revoked by the identity that requested it.");
}

public static DomainError CannotRequestReactivationWhenThereIsAnOpenReactivationRequest()
{
return new DomainError("error.platform.validation.relationshipRequest.cannotRequestReactivationWhenThereIsAnOpenReactivationRequest",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Backbone.BuildingBlocks.Domain.Events;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Relationships.Domain.Aggregates.Relationships;

namespace Backbone.Modules.Relationships.Domain.DomainEvents.Outgoing;
public class RelationshipReactivationCompletedDomainEvent : DomainEvent
{
public RelationshipReactivationCompletedDomainEvent(Relationship relationship, IdentityAddress peer)
{
RelationshipId = relationship.Id;
Peer = peer.Value;
}

public string RelationshipId { get; }
public string Peer { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Backbone.Modules.Relationships.Domain.DomainEvents.Outgoing;
public class RelationshipReactivationRequestedDomainEvent : DomainEvent
{
public RelationshipReactivationRequestedDomainEvent(Relationship relationship, IdentityAddress requestingIdentity, IdentityAddress peer) :
base($"{relationship.Id}/ReactivationRequested/{relationship.AuditLog.Last().CreatedAt}")
base($"{relationship.Id}/ReactivationRequested/{relationship.AuditLog.OrderBy(a => a.CreatedAt).Last().CreatedAt}")
{
RelationshipId = relationship.Id;
RequestingIdentity = requestingIdentity.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Backbone.Modules.Relationships.Domain.DomainEvents.Outgoing;

public class RelationshipStatusChangedDomainEvent : DomainEvent
{
public RelationshipStatusChangedDomainEvent(Relationship relationship) : base($"{relationship.Id}/StatusChanged/{relationship.AuditLog.Last().CreatedAt.ToUniversalString()}")
public RelationshipStatusChangedDomainEvent(Relationship relationship) : base($"{relationship.Id}/StatusChanged/{relationship.AuditLog.OrderBy(a => a.CreatedAt).Last().CreatedAt.ToUniversalString()}")
{
RelationshipId = relationship.Id;
Status = relationship.Status.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using Microsoft.EntityFrameworkCore;

namespace Backbone.Modules.Relationships.Infrastructure.Persistence.Database.Repository;

public class RelationshipsRepository : IRelationshipsRepository
{
private readonly DbSet<Relationship> _relationships;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,22 @@ public static Relationship CreateTerminatedRelationship()

return relationship;
}

public static Relationship CreateTerminatedRelationship(IdentityAddress activeIdentity, IdentityAddress? to = null)
{
to ??= TestDataGenerator.CreateRandomIdentityAddress();

var relationship = CreateActiveRelationship(activeIdentity, to);
relationship.Terminate(relationship.From, TestDataGenerator.CreateRandomDeviceId());

return relationship;
}

public static Relationship CreateRelationshipWithRequestedReactivation(IdentityAddress from, IdentityAddress to, IdentityAddress reactivationRequestedBy)
{
var relationship = CreateTerminatedRelationship(from, to);
relationship.RequestReactivation(reactivationRequestedBy, TestDataGenerator.CreateRandomDeviceId());

return relationship;
}
}
Loading

0 comments on commit 6f90d49

Please sign in to comment.