Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set UpdatedEntity on an import notification when a linked movement is being updated #97

Merged
merged 10 commits into from
Feb 4, 2025
2 changes: 2 additions & 0 deletions Btms.Backend.Data/IMongoCollectionSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public interface IMongoCollectionSet<T> : IQueryable<T> where T : IDataEntity
Task Insert(T item, CancellationToken cancellationToken = default);

Task Update(T item, CancellationToken cancellationToken = default);

Task Update(List<T> items, CancellationToken cancellationToken = default);

Task Update(T item, string etag, CancellationToken cancellationToken = default);

Expand Down
8 changes: 8 additions & 0 deletions Btms.Backend.Data/InMemory/MemoryCollectionSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ public Task Update(T item, CancellationToken cancellationToken = default)
return Update(item, item._Etag, cancellationToken);
}

public async Task Update(List<T> items, CancellationToken cancellationToken = default)
{
foreach (var item in items)
{
await Update(item, cancellationToken);
}
}

[SuppressMessage("SonarLint", "S2955",
Justification =
"IEquatable<T> would need to be implemented on every data entity just to stop sonar complaining about a null check. Nope.")]
Expand Down
27 changes: 18 additions & 9 deletions Btms.Backend.Data/Mongo/MongoCollectionSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
namespace Btms.Backend.Data.Mongo;

public class MongoCollectionSet<T>(MongoDbContext dbContext, string collectionName = null!)
: IMongoCollectionSet<T> where T : IDataEntity
: IMongoCollectionSet<T> where T : class, IDataEntity
{
private readonly IMongoCollection<T> collection = string.IsNullOrEmpty(collectionName)
private readonly IMongoCollection<T> _collection = string.IsNullOrEmpty(collectionName)
? dbContext.Database.GetCollection<T>(typeof(T).Name)
: dbContext.Database.GetCollection<T>(collectionName);

private readonly List<T> _entitiesToInsert = [];
private readonly List<(T Item, string Etag)> _entitiesToUpdate = [];

private IMongoQueryable<T> EntityQueryable => collection.AsQueryable();
private IMongoQueryable<T> EntityQueryable => _collection.AsQueryable();

public IEnumerator<T> GetEnumerator()
{
Expand Down Expand Up @@ -50,9 +50,9 @@ public async Task PersistAsync(CancellationToken cancellationToken)
foreach (var item in _entitiesToInsert)
{
item._Etag = BsonObjectIdGenerator.Instance.GenerateId(null, null).ToString()!;
item.Created = DateTime.UtcNow;
item.UpdatedEntity = DateTime.UtcNow;
await collection.InsertOneAsync(dbContext.ActiveTransaction?.Session, item, cancellationToken: cancellationToken);
item.Created = item.UpdatedEntity = DateTime.UtcNow;

await _collection.InsertOneAsync(dbContext.ActiveTransaction?.Session, item, cancellationToken: cancellationToken);
}

_entitiesToInsert.Clear();
Expand All @@ -68,11 +68,12 @@ public async Task PersistAsync(CancellationToken cancellationToken)

item.Item._Etag = BsonObjectIdGenerator.Instance.GenerateId(null, null).ToString()!;
item.Item.UpdatedEntity = DateTime.UtcNow;

var session = dbContext.ActiveTransaction?.Session;
var updateResult = session is not null
? await collection.ReplaceOneAsync(session, filter, item.Item,
? await _collection.ReplaceOneAsync(session, filter, item.Item,
cancellationToken: cancellationToken)
: await collection.ReplaceOneAsync(filter, item.Item,
: await _collection.ReplaceOneAsync(filter, item.Item,
cancellationToken: cancellationToken);

if (updateResult.ModifiedCount == 0)
Expand All @@ -96,6 +97,14 @@ public async Task Update(T item, CancellationToken cancellationToken = default)
await Update(item, item._Etag, cancellationToken);
}

public async Task Update(List<T> items, CancellationToken cancellationToken = default)
{
foreach (var item in items)
{
await Update(item, cancellationToken);
}
}

public Task Update(T item, string etag, CancellationToken cancellationToken = default)
{
if (_entitiesToInsert.Exists(x => x.Id == item.Id))
Expand All @@ -111,6 +120,6 @@ public Task Update(T item, string etag, CancellationToken cancellationToken = d

public IAggregateFluent<T> Aggregate()
{
return collection.Aggregate();
return _collection.Aggregate();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"serviceHeader": {
"sourceSystem": "CDSSIM",
"destinationSystem": "ALVS",
"correlationId": "000",
"serviceCallTimestamp": "2024-10-04T13:58:09.5864286Z"
},
"header": {
"entryReference": "CHEDAGB20241041389",
"entryVersionNumber": 3,
"previousVersionNumber": 2,
"declarationUCR": "SimCHEDA.GB.2024.1041389",
"declarationPartNumber": null,
"declarationType": null,
"arrivalDateTime": null,
"submitterTURN": null,
"declarantId": null,
"declarantName": "CDS_Simulator",
"dispatchCountryCode": null,
"goodsLocationCode": "BELBELGVM",
"masterUCR": "SimCHEDA.GB.2024.1041389"
},
"items": [
{
"clearanceRequestReference": null,
"itemNumber": 1,
"customsProcedureCode": null,
"taricCommodityCode": null,
"goodsDescription": null,
"consigneeId": null,
"consigneeName": null,
"itemNetMass": null,
"itemSupplementaryUnits": null,
"itemThirdQuantity": null,
"itemOriginCountryCode": null,
"documents": [
{
"documentCode": "C640",
"documentReference": "GBCHD2024.1041389",
"documentStatus": "P",
"documentControl": null,
"documentQuantity": 3
}
],
"check": [
{
"checkCode": "H2019",
"departmentCode": "GB"
}
]
}
]
}
1 change: 1 addition & 0 deletions Btms.Backend.IntegrationTests/Fixtures/Linking/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The intention is that this data follows the SmokeTest data to provide additional updates that can be applied after the smoke test data has been imported.
48 changes: 45 additions & 3 deletions Btms.Backend.IntegrationTests/LinkingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ await Client.MakeSyncClearanceRequest(new SyncClearanceRequestsCommand
public async Task SyncClearanceRequests_WithReferencedNotifications_ShouldLink()
{
// Arrange
await base.ClearDb();
await ClearDb();

// Act
await Client.MakeSyncNotificationsRequest(new SyncNotificationsCommand
Expand All @@ -60,7 +60,7 @@ await Client.MakeSyncClearanceRequest(new SyncClearanceRequestsCommand
public async Task SyncNotifications_WithNoReferencedMovements_ShouldNotLink()
{
// Arrange
await base.ClearDb();
await ClearDb();

// Act
await Client.MakeSyncNotificationsRequest(new SyncNotificationsCommand
Expand All @@ -81,7 +81,7 @@ await Client.MakeSyncNotificationsRequest(new SyncNotificationsCommand
public async Task SyncNotifications_WithReferencedMovements_ShouldLink()
{
// Arrange
await base.ClearDb();
await ClearDb();

// Act
await Client.MakeSyncClearanceRequest(new SyncClearanceRequestsCommand
Expand All @@ -101,4 +101,46 @@ await Client.MakeSyncNotificationsRequest(new SyncNotificationsCommand
.Any(x => x.Value is { Links: not null })
.Should().Be(true);
}

[Fact]
public async Task ImportNotification_ResourceUpdated_UpdatedFieldOnResource_ShouldNotChange()
{
await ClearDb();

// Import notifications
await Client.MakeSyncNotificationsRequest(new SyncNotificationsCommand
{
SyncPeriod = SyncPeriod.All, RootFolder = "SmokeTest"
});

var document = Client.AsJsonApiClient().GetById("CHEDA.GB.2024.1041389", "api/import-notifications");
var updated = DateTime.Parse((document.Data.Attributes?["updated"]!).ToString()!);
var updatedEntity = DateTime.Parse((document.Data.Attributes?["updatedEntity"]!).ToString()!);

// Import clearance requests and initial linking will take place
await Client.MakeSyncClearanceRequest(new SyncClearanceRequestsCommand
{
SyncPeriod = SyncPeriod.All, RootFolder = "SmokeTest"
});

document = Client.AsJsonApiClient().GetById("CHEDA.GB.2024.1041389", "api/import-notifications");
var updatedPostLink = DateTime.Parse((document.Data.Attributes?["updated"]!).ToString()!);
var updatedEntityPostLink = DateTime.Parse((document.Data.Attributes?["updatedEntity"]!).ToString()!);

updated.Should().Be(updatedPostLink);
updatedEntity.Should().BeBefore(updatedEntityPostLink);

// Import new clearance version, link will already exist, but UpdateEntity will still change
await Client.MakeSyncClearanceRequest(new SyncClearanceRequestsCommand
{
SyncPeriod = SyncPeriod.All, RootFolder = "Linking"
});

document = Client.AsJsonApiClient().GetById("CHEDA.GB.2024.1041389", "api/import-notifications");
var updatedPostMovementUpdate = DateTime.Parse((document.Data.Attributes?["updated"]!).ToString()!);
var updatedEntityPostMovementUpdate = DateTime.Parse((document.Data.Attributes?["updatedEntity"]!).ToString()!);

updatedPostLink.Should().Be(updatedPostMovementUpdate);
updatedEntityPostLink.Should().BeBefore(updatedEntityPostMovementUpdate);
}
}
88 changes: 33 additions & 55 deletions Btms.Consumers.Tests/ClearanceRequestConsumerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Btms.Backend.Data.InMemory;
using Btms.Backend.Data;
using Btms.Business.Builders;
using Btms.Business.Pipelines.PreProcessing;
using Btms.Business.Services.Decisions;
Expand All @@ -21,6 +21,13 @@ namespace Btms.Consumers.Tests;

public class ClearanceRequestConsumerTests
{
private readonly ILinkingService _mockLinkingService = Substitute.For<ILinkingService>();
private readonly IDecisionService _decisionService = Substitute.For<IDecisionService>();
private readonly IMatchingService _matchingService = Substitute.For<IMatchingService>();
private readonly IValidationService _validationService = Substitute.For<IValidationService>();
private readonly IMongoDbContext _mongoDbContext = Substitute.For<IMongoDbContext>();
private readonly IPreProcessor<AlvsClearanceRequest, Movement> _preProcessor = Substitute.For<IPreProcessor<AlvsClearanceRequest, Movement>>();

[Theory]
[InlineData(PreProcessingOutcome.New)]
[InlineData(PreProcessingOutcome.Skipped)]
Expand All @@ -32,91 +39,62 @@ public async Task WhenPreProcessingSucceeds_AndLastAuditEntryIsLinked_ThenLinkSh
var clearanceRequest = CreateAlvsClearanceRequest();
var mbFactory = new MovementBuilderFactory(NullLogger<MovementBuilder>.Instance);
var mb = mbFactory.From(AlvsClearanceRequestMapper.Map(clearanceRequest));

mb.Update(AuditEntry.CreateLinked("Test", 1));

var movement = mb.Build();

var mockLinkingService = Substitute.For<ILinkingService>();
var decisionService = Substitute.For<IDecisionService>();
var matchingService = Substitute.For<IMatchingService>();
var validationService = Substitute.For<IValidationService>();
var preProcessor = Substitute.For<IPreProcessor<AlvsClearanceRequest, Model.Movement>>();

preProcessor.Process(Arg.Any<PreProcessingContext<AlvsClearanceRequest>>())
_preProcessor.Process(Arg.Any<PreProcessingContext<AlvsClearanceRequest>>())
.Returns(Task.FromResult(new PreProcessingResult<Movement>(outcome, movement, null)));

var consumer =
new AlvsClearanceRequestConsumer(preProcessor, mockLinkingService, matchingService, decisionService, validationService, NullLogger<AlvsClearanceRequestConsumer>.Instance, null!)
{
Context = new ConsumerContext
{
Headers = new Dictionary<string, object>
{
{ "messageId", clearanceRequest.Header!.EntryReference! }
}
}
};
var consumer = CreateSubject(clearanceRequest.Header!.EntryReference!);

// ACT
await consumer.OnHandle(clearanceRequest, CancellationToken.None);

// ASSERT
consumer.Context.IsLinked().Should().BeFalse();

await mockLinkingService.DidNotReceive().Link(Arg.Any<LinkContext>(), Arg.Any<CancellationToken>());
await _mockLinkingService.DidNotReceive().Link(Arg.Any<LinkContext>(), Arg.Any<CancellationToken>());
}

[Fact]
public async Task WhenPreProcessingSucceeds_AndLastAuditEntryIsCreated_ThenLinkShouldBeRun()
{
// ARRANGE
var mbFactory = new MovementBuilderFactory(NullLogger<MovementBuilder>.Instance);
var clearanceRequest = CreateAlvsClearanceRequest();

var mbFactory = new MovementBuilderFactory(NullLogger<MovementBuilder>.Instance);
var mb = mbFactory.From(AlvsClearanceRequestMapper.Map(clearanceRequest));

mb.Update(mb.CreateAuditEntry("Test", CreatedBySystem.Cds));

var movement = mb.Build();

var mockLinkingService = Substitute.For<ILinkingService>();
var decisionService = Substitute.For<IDecisionService>();
var matchingService = Substitute.For<IMatchingService>();
var validationService = Substitute.For<IValidationService>();
var preProcessor = Substitute.For<IPreProcessor<AlvsClearanceRequest, Model.Movement>>();

mockLinkingService.Link(Arg.Any<LinkContext>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new LinkResult(LinkOutcome.Linked)));

preProcessor.Process(Arg.Any<PreProcessingContext<AlvsClearanceRequest>>())
_preProcessor.Process(Arg.Any<PreProcessingContext<AlvsClearanceRequest>>())
.Returns(Task.FromResult(new PreProcessingResult<Movement>(PreProcessingOutcome.New, movement, null)));

var consumer =
new AlvsClearanceRequestConsumer(preProcessor, mockLinkingService, matchingService, decisionService, validationService, NullLogger<AlvsClearanceRequestConsumer>.Instance, new MemoryMongoDbContext())
{
Context = new ConsumerContext
{
Headers = new Dictionary<string, object>
{
{ "messageId", clearanceRequest.Header!.EntryReference! }
}
}
};
_mockLinkingService.Link(Arg.Any<LinkContext>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new LinkResult(LinkOutcome.Linked)));
var consumer = CreateSubject(clearanceRequest.Header!.EntryReference!);

// ACT
await consumer.OnHandle(clearanceRequest, CancellationToken.None);

// ASSERT
consumer.Context.IsPreProcessed().Should().BeTrue();
consumer.Context.IsLinked().Should().BeTrue();

await mockLinkingService.Received().Link(Arg.Any<LinkContext>(), Arg.Any<CancellationToken>());
await _mockLinkingService.Received().Link(Arg.Any<LinkContext>(), Arg.Any<CancellationToken>());
}

private static AlvsClearanceRequest CreateAlvsClearanceRequest()
{
return ClearanceRequestBuilder.Default()
.WithValidDocumentReferenceNumbers().Build();
}
}

private AlvsClearanceRequestConsumer CreateSubject(string messageId)
{
return new AlvsClearanceRequestConsumer(_preProcessor, _mockLinkingService, _matchingService, _decisionService,
_validationService, _mongoDbContext, NullLogger<AlvsClearanceRequestConsumer>.Instance)
{
Context = new ConsumerContext
{
Headers = new Dictionary<string, object>
{
{ "messageId", messageId }
}
}
};
}
}
Loading
Loading