diff --git a/src/KeeperData.Api/appsettings.json b/src/KeeperData.Api/appsettings.json index 1930eb5b..81fee1da 100644 --- a/src/KeeperData.Api/appsettings.json +++ b/src/KeeperData.Api/appsettings.json @@ -67,7 +67,8 @@ "SamHoldersEnabled": true, "SamHerdsEnabled": true, "SamPartiesEnabled": true, - "SamCommonLandsEnabled": true + "SamCommonLandsEnabled": true, + "SamShowgroundsEnabled": true }, "DataBridgeScanConfiguration": { "QueryPageSize": 100, diff --git a/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Bulk/SamBulkScanContext.cs b/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Bulk/SamBulkScanContext.cs index c3aaba2c..d4616901 100644 --- a/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Bulk/SamBulkScanContext.cs +++ b/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Bulk/SamBulkScanContext.cs @@ -10,4 +10,5 @@ public class SamBulkScanContext : ScanContext, IBulkScanContext public int PageSize { get; init; } = 100; public EntityScanContext Holders { get; init; } = new(); public EntityScanContext Holdings { get; init; } = new(); + public EntityScanContext Showgrounds { get; init; } = new(); } \ No newline at end of file diff --git a/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Bulk/Steps/SamShowgroundBulkScanStep.cs b/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Bulk/Steps/SamShowgroundBulkScanStep.cs new file mode 100644 index 00000000..a89f8a37 --- /dev/null +++ b/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Bulk/Steps/SamShowgroundBulkScanStep.cs @@ -0,0 +1,64 @@ +using KeeperData.Application.Orchestration.ChangeScanning.BaseClasses; +using KeeperData.Core.ApiClients.DataBridgeApi; +using KeeperData.Core.ApiClients.DataBridgeApi.Configuration; +using KeeperData.Core.ApiClients.DataBridgeApi.Contracts; +using KeeperData.Core.Attributes; +using KeeperData.Core.Messaging.Contracts.V1.Sam; +using KeeperData.Core.Messaging.MessagePublishers; +using KeeperData.Core.Messaging.MessagePublishers.Clients; +using KeeperData.Core.Providers; +using Microsoft.Extensions.Logging; + +namespace KeeperData.Application.Orchestration.ChangeScanning.Sam.Bulk.Steps; + +[StepOrder(2)] +public class SamShowgroundBulkScanStep( + IDataBridgeClient dataBridgeClient, + IMessagePublisher intakeMessagePublisher, + DataBridgeScanConfiguration dataBridgeScanConfiguration, + IDelayProvider delayProvider, + ILogger logger) + : BulkScanStepBase( + dataBridgeClient, + intakeMessagePublisher, + dataBridgeScanConfiguration, + delayProvider, + logger) +{ + protected override string SelectFields => "CPH"; + protected override string OrderBy => "CPH asc"; + + protected override EntityScanContext GetScanContext(SamBulkScanContext context) => context.Showgrounds; + protected override async Task> GetHoldingsAsync( + int top, + int skip, + string selectFields, + DateTime? updatedSince, + string orderBy, + CancellationToken cancellationToken) + { + var result = await DataBridgeClient.GetSamShowgroundsAsync( + top, + skip, + selectFields, + updatedSince, + orderBy, + cancellationToken); + + return result ?? new DataBridgeResponse { CollectionName = "SamShowgrounds" }; + } + + protected override string ExtractIdentifier(SamScanShowgroundIdentifier holdingIdentifier) + { + return holdingIdentifier.CPH; + } + + protected override SamImportHoldingMessage CreateImportMessage(string identifier) + { + return new SamImportHoldingMessage + { + Id = Guid.NewGuid(), + Identifier = identifier + }; + } +} \ No newline at end of file diff --git a/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Daily/SamDailyScanContext.cs b/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Daily/SamDailyScanContext.cs index 1f1c9f72..9417c544 100644 --- a/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Daily/SamDailyScanContext.cs +++ b/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Daily/SamDailyScanContext.cs @@ -11,4 +11,5 @@ public class SamDailyScanContext : ScanContext public EntityScanContext Parties { get; init; } = new(); public EntityScanContext Ports { get; init; } = new(); public EntityScanContext CommonLands { get; init; } = new(); + public EntityScanContext Showgrounds { get; init; } = new(); } \ No newline at end of file diff --git a/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Daily/Steps/SamShowgroundDailyScanStep.cs b/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Daily/Steps/SamShowgroundDailyScanStep.cs new file mode 100644 index 00000000..1c120ed9 --- /dev/null +++ b/src/KeeperData.Application/Orchestration/ChangeScanning/Sam/Daily/Steps/SamShowgroundDailyScanStep.cs @@ -0,0 +1,63 @@ +using KeeperData.Application.Orchestration.ChangeScanning.BaseClasses; +using KeeperData.Core.ApiClients.DataBridgeApi; +using KeeperData.Core.ApiClients.DataBridgeApi.Configuration; +using KeeperData.Core.ApiClients.DataBridgeApi.Contracts; +using KeeperData.Core.Attributes; +using KeeperData.Core.Messaging.Contracts.V1.Sam; +using KeeperData.Core.Messaging.MessagePublishers; +using KeeperData.Core.Messaging.MessagePublishers.Clients; +using KeeperData.Core.Providers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace KeeperData.Application.Orchestration.ChangeScanning.Sam.Daily.Steps; + +[StepOrder(6)] +public class SamShowgroundDailyScanStep( + IDataBridgeClient dataBridgeClient, + IMessagePublisher intakeMessagePublisher, + DataBridgeScanConfiguration dataBridgeScanConfiguration, + IDelayProvider delayProvider, + IConfiguration configuration, + ILogger logger) + : DailyScanStepBase(dataBridgeClient, intakeMessagePublisher, dataBridgeScanConfiguration, + delayProvider, configuration, logger) +{ + private const string SelectFields = "CPH"; + private const string OrderBy = "CPH asc"; + + protected override bool IsEntityEnabled() + => Configuration.GetValue("DataBridgeCollectionFlags:SamShowgroundsEnabled"); + + protected override EntityScanContext GetScanContext(SamDailyScanContext context) + => context.Showgrounds; + + protected override async Task?> QueryDataAsync( + SamDailyScanContext context, + CancellationToken cancellationToken) + => await DataBridgeClient.GetSamShowgroundsAsync( + context.Showgrounds.CurrentTop, + context.Showgrounds.CurrentSkip, + SelectFields, + context.UpdatedSinceDateTime, + OrderBy, + cancellationToken); + + protected override async Task PublishMessagesAsync( + DataBridgeResponse queryResponse, + CancellationToken cancellationToken) + { + var identifiers = queryResponse.Data + .Select(x => x.CPH) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct() + .ToList(); + + foreach (var id in identifiers) + { + var message = new SamUpdateHoldingMessage { Id = Guid.NewGuid(), Identifier = id }; + + await IntakeMessagePublisher.PublishAsync(message, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/SamHoldingImportContext.cs b/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/SamHoldingImportContext.cs index 6d0c2d36..e624ef64 100644 --- a/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/SamHoldingImportContext.cs +++ b/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/SamHoldingImportContext.cs @@ -34,4 +34,5 @@ public class SamHoldingImportContext public List PartiesWithNoRelationshipToSiteToClean { get; set; } = []; public List RawPorts { get; set; } = []; + public List RawShowgrounds { get; set; } = []; } \ No newline at end of file diff --git a/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportAggregationStep.cs b/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportAggregationStep.cs index 0d808f85..10ea88f6 100644 --- a/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportAggregationStep.cs +++ b/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportAggregationStep.cs @@ -20,6 +20,7 @@ protected override async Task ExecuteCoreAsync(SamHoldingImportContext context, var getHerdsTask = _dataBridgeClient.GetSamHerdsAsync(context.Cph, cancellationToken); var getPortsTask = _dataBridgeClient.GetSamPortsAsync(context.Cph, cancellationToken); var getCommonLandsByCommonCphTask = _dataBridgeClient.GetSamCommonLandsByCommonCphAsync(context.Cph, cancellationToken); + var getShowgroundsTask = _dataBridgeClient.GetSamShowgroundsByCphAsync(context.Cph, cancellationToken); await Task.WhenAll( getHoldingsTask, @@ -40,6 +41,7 @@ await Task.WhenAll( var parties = await GetSamPartiesAsync(context, cancellationToken); context.RawParties = SamPartyMapper.AggregatePartyAndHolder(parties, context.RawHolders); + context.RawShowgrounds = getShowgroundsTask.Result; } private async Task> GetSamPartiesAsync(SamHoldingImportContext context, CancellationToken cancellationToken) diff --git a/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportGoldMappingStep.cs b/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportGoldMappingStep.cs index bafb2992..3120fbee 100644 --- a/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportGoldMappingStep.cs +++ b/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportGoldMappingStep.cs @@ -60,6 +60,7 @@ protected override async Task ExecuteCoreAsync(SamHoldingImportContext context, context.SilverHoldings, context.GoldSiteGroupMarks, context.GoldParties, + context.RawShowgrounds, countryIdentifierLookupService.GetByIdAsync, siteTypeLookupService.GetByCodeAsync, siteIdentifierTypeLookupService.GetByCodeAsync, diff --git a/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamHoldingMapper.cs b/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamHoldingMapper.cs index ac504b9b..198966c2 100644 --- a/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamHoldingMapper.cs +++ b/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamHoldingMapper.cs @@ -56,7 +56,6 @@ public static async Task ToSilver( var result = new SamHoldingDocument { // Id - Leave to support upsert assigning Id - LastUpdatedBatchId = h.BATCH_ID, CreatedDate = h.CreatedAtUtc ?? DateTime.UtcNow, LastUpdatedDate = h.UpdatedAtUtc ?? DateTime.UtcNow, @@ -109,10 +108,8 @@ public static async Task ToSilver( AddressTown = h.TOWN, AddressPostCode = h.POSTCODE, CountrySubDivision = h.UK_INTERNAL_CODE, - CountryIdentifier = countryId, CountryCode = countryCode, - UniquePropertyReferenceNumber = h.UDPRN } }, @@ -184,6 +181,7 @@ public static SamHoldingDocument SelectAddressSource(List si List silverHoldings, List goldSiteGroupMarks, List goldParties, + List rawShowgrounds, Func> getCountryById, Func> getSiteTypeByCode, Func> getSiteIdentifierTypeByCode, @@ -233,6 +231,22 @@ public static SamHoldingDocument SelectAddressSource(List si cphnSiteIdentifierTypeDocument.Name, cphnSiteIdentifierTypeDocument.LastModifiedDate); + var showground = rawShowgrounds?.FirstOrDefault(); + DateTime? effectiveFromDate = null; + DateTime? effectiveToDate = null; + bool? approvalCurrentFlag = null; + + if (showground != null) + { + effectiveFromDate = showground.START_DATE; + effectiveToDate = showground.END_DATE; + var now = DateTime.UtcNow; + + approvalCurrentFlag = + (effectiveFromDate == null || now >= effectiveFromDate.Value) + && (effectiveToDate == null || now <= effectiveToDate.Value); + } + var site = existingSite is not null ? await UpdateSiteAsync( representative, @@ -245,6 +259,9 @@ public static SamHoldingDocument SelectAddressSource(List si allDerivedActivities, derivedSiteType, cphnSiteIdentifierType, + effectiveFromDate, + effectiveToDate, + approvalCurrentFlag, cancellationToken) : await CreateSiteAsync( goldSiteId, @@ -257,6 +274,9 @@ public static SamHoldingDocument SelectAddressSource(List si allDerivedActivities, derivedSiteType, cphnSiteIdentifierType, + effectiveFromDate, + effectiveToDate, + approvalCurrentFlag, cancellationToken); return SiteDocument.FromDomain(site); @@ -367,6 +387,9 @@ private static async Task CreateSiteAsync( List activities, SiteType? siteType, SiteIdentifierType? siteIdentifierType, + DateTime? effectiveFromDate, + DateTime? effectiveToDate, + bool? approvalCurrentFlag, CancellationToken cancellationToken) { var (address, communication) = await ResolveLocationPartsAsync(addressSource, getCountryById, cancellationToken); @@ -394,7 +417,10 @@ private static async Task CreateSiteAsync( representative.CphTypeIdentifier, siteType, location, - isPermanentLandHolding ? representative.SecondaryCph : null); + representative.CphRelationshipType.IsPermanentLandHolding() ? representative.SecondaryCph : null, + effectiveFromDate, + effectiveToDate, + approvalCurrentFlag); ApplySiteData(site, goldSiteId, representative, goldSiteGroupMarks, goldParties, species, activities, siteIdentifierType); @@ -412,6 +438,9 @@ private static async Task UpdateSiteAsync( List activities, SiteType? siteType, SiteIdentifierType? siteIdentifierType, + DateTime? effectiveFromDate, + DateTime? effectiveToDate, + bool? approvalCurrentFlag, CancellationToken cancellationToken) { var isPermanentLandHolding = representative.CphRelationshipType.IsPermanentLandHolding(); @@ -428,7 +457,10 @@ private static async Task UpdateSiteAsync( representative.Deleted, isPermanentLandHolding ? null : representative.SecondaryCph, representative.CphTypeIdentifier, - isPermanentLandHolding ? representative.SecondaryCph : null); + representative.CphRelationshipType.IsPermanentLandHolding() ? representative.SecondaryCph : null, + effectiveFromDate, + effectiveToDate, + approvalCurrentFlag); var (updatedAddress, updatedCommunication) = await ResolveLocationPartsAsync(addressSource, getCountryById, cancellationToken); diff --git a/src/KeeperData.Core/ApiClients/DataBridgeApi/Contracts/SamShowground.cs b/src/KeeperData.Core/ApiClients/DataBridgeApi/Contracts/SamShowground.cs new file mode 100644 index 00000000..7a58fde8 --- /dev/null +++ b/src/KeeperData.Core/ApiClients/DataBridgeApi/Contracts/SamShowground.cs @@ -0,0 +1,24 @@ +using KeeperData.Core.ApiClients.DataBridgeApi.Converters; +using System.Text.Json.Serialization; + +namespace KeeperData.Core.ApiClients.DataBridgeApi.Contracts; + +public class SamShowground : BronzeBase +{ + [JsonPropertyName("CPH")] + public string CPH { get; set; } = string.Empty; + + [JsonPropertyName("START_DATE")] + [JsonConverter(typeof(SafeNullableDateTimeConverter))] + public DateTime? START_DATE { get; set; } + + [JsonPropertyName("END_DATE")] + [JsonConverter(typeof(SafeNullableDateTimeConverter))] + public DateTime? END_DATE { get; set; } +} + +public class SamScanShowgroundIdentifier +{ + [JsonPropertyName("CPH")] + public string CPH { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/KeeperData.Core/ApiClients/DataBridgeApi/DataBridgeApiRoutes.cs b/src/KeeperData.Core/ApiClients/DataBridgeApi/DataBridgeApiRoutes.cs index 1a1ba895..c07db9e0 100644 --- a/src/KeeperData.Core/ApiClients/DataBridgeApi/DataBridgeApiRoutes.cs +++ b/src/KeeperData.Core/ApiClients/DataBridgeApi/DataBridgeApiRoutes.cs @@ -12,4 +12,5 @@ public static class DataBridgeApiRoutes public const string GetSamHerds = "api/query/sam_herd"; public const string GetSamPorts = "api/query/amls2_port"; public const string GetSamCommonLands = "api/query/amls2_common_land"; + public const string GetSamShowgrounds = "api/query/sam_showground"; } \ No newline at end of file diff --git a/src/KeeperData.Core/ApiClients/DataBridgeApi/DataBridgeQueries.cs b/src/KeeperData.Core/ApiClients/DataBridgeApi/DataBridgeQueries.cs index f71c3dc6..71c18543 100644 --- a/src/KeeperData.Core/ApiClients/DataBridgeApi/DataBridgeQueries.cs +++ b/src/KeeperData.Core/ApiClients/DataBridgeApi/DataBridgeQueries.cs @@ -161,4 +161,11 @@ public static Dictionary SamPortsByCph(string id) [FilterKey] = $"CPH eq '{id}'" }; } + public static Dictionary SamShowgroundsByCph(string id) + { + return new Dictionary + { + [FilterKey] = $"CPH eq '{id}'" + }; + } } \ No newline at end of file diff --git a/src/KeeperData.Core/ApiClients/DataBridgeApi/IDataBridgeClient.cs b/src/KeeperData.Core/ApiClients/DataBridgeApi/IDataBridgeClient.cs index 023b0bf3..e9e6cfea 100644 --- a/src/KeeperData.Core/ApiClients/DataBridgeApi/IDataBridgeClient.cs +++ b/src/KeeperData.Core/ApiClients/DataBridgeApi/IDataBridgeClient.cs @@ -93,4 +93,13 @@ public interface IDataBridgeClient string? orderBy = null, CancellationToken cancellationToken = default); Task> GetSamCommonLandsByCommonCphAsync(string cph, CancellationToken cancellationToken); + + Task?> GetSamShowgroundsAsync( + int top, + int skip, + string? selectFields = null, + DateTime? updatedSinceDateTime = null, + string? orderBy = null, + CancellationToken cancellationToken = default); + Task> GetSamShowgroundsByCphAsync(string cph, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/KeeperData.Core/DTOs/SiteDto.cs b/src/KeeperData.Core/DTOs/SiteDto.cs index 01fe7720..a00cb083 100644 --- a/src/KeeperData.Core/DTOs/SiteDto.cs +++ b/src/KeeperData.Core/DTOs/SiteDto.cs @@ -115,4 +115,13 @@ public class SiteDto public List AssociatedCommonLands { get; set; } = []; [JsonPropertyName("permanentLandHoldingIdentifier")] public string? PermanentLandHoldingIdentifier { get; set; } + + [JsonPropertyName("effectiveFromDate")] + public DateTime? EffectiveFromDate { get; set; } + + [JsonPropertyName("effectiveToDate")] + public DateTime? EffectiveToDate { get; set; } + + [JsonPropertyName("approvalCurrentFlag")] + public bool? ApprovalCurrentFlag { get; set; } } \ No newline at end of file diff --git a/src/KeeperData.Core/Documents/SiteDocument.cs b/src/KeeperData.Core/Documents/SiteDocument.cs index f9200fd8..fbddb5a1 100644 --- a/src/KeeperData.Core/Documents/SiteDocument.cs +++ b/src/KeeperData.Core/Documents/SiteDocument.cs @@ -159,6 +159,18 @@ public class SiteDocument : IEntity, IDeletableEntity, IContainsIndexes [JsonPropertyName("associatedCommonLands")] public List AssociatedCommonLands { get; set; } = []; + [BsonElement("effectiveFromDate")] + [JsonPropertyName("effectiveFromDate")] + public DateTime? EffectiveFromDate { get; set; } + + [BsonElement("effectiveToDate")] + [JsonPropertyName("effectiveToDate")] + public DateTime? EffectiveToDate { get; set; } + + [BsonElement("approvalCurrentFlag")] + [JsonPropertyName("approvalCurrentFlag")] + public bool? ApprovalCurrentFlag { get; set; } + public static SiteDocument FromDomain(Site m) => new() { Id = m.Id, @@ -180,7 +192,10 @@ public class SiteDocument : IEntity, IDeletableEntity, IContainsIndexes Activities = [.. m.Activities.Select(SiteActivityDocument.FromDomain)], ParentSiteIdentifier = m.ParentSiteIdentifier, HoldingType = m.HoldingType, - PermanentLandHoldingIdentifier = m.PermanentLandHoldingIdentifier + PermanentLandHoldingIdentifier = m.PermanentLandHoldingIdentifier, + EffectiveFromDate = m.EffectiveFromDate, + EffectiveToDate = m.EffectiveToDate, + ApprovalCurrentFlag = m.ApprovalCurrentFlag }; public Site ToDomain() @@ -200,7 +215,10 @@ public Site ToDomain() Location?.ToDomain(), ParentSiteIdentifier, HoldingType, - PermanentLandHoldingIdentifier + PermanentLandHoldingIdentifier, + EffectiveFromDate, + EffectiveToDate, + ApprovalCurrentFlag ); foreach (var si in Identifiers) diff --git a/src/KeeperData.Core/Documents/SiteDocumentExtensions.cs b/src/KeeperData.Core/Documents/SiteDocumentExtensions.cs index 4790e8e3..8b09bb67 100644 --- a/src/KeeperData.Core/Documents/SiteDocumentExtensions.cs +++ b/src/KeeperData.Core/Documents/SiteDocumentExtensions.cs @@ -32,7 +32,10 @@ public static class SiteDocumentExtensions PermanentLandHoldingIdentifier = doc.PermanentLandHoldingIdentifier, LocalAuthorityName = doc.LocalAuthorityName, AssociatedMainHoldings = doc.AssociatedMainHoldings?.Select(h => h.ToDto()).ToList() ?? [], - AssociatedCommonLands = doc.AssociatedCommonLands?.Select(h => h.ToDto()).ToList() ?? [] + AssociatedCommonLands = doc.AssociatedCommonLands?.Select(h => h.ToDto()).ToList() ?? [], + EffectiveFromDate = doc.EffectiveFromDate, + EffectiveToDate = doc.EffectiveToDate, + ApprovalCurrentFlag = doc.ApprovalCurrentFlag }; private static AssociatedHoldingDto ToDto(this AssociatedHoldingDocument doc) => new() diff --git a/src/KeeperData.Core/Domain/Sites/Site.cs b/src/KeeperData.Core/Domain/Sites/Site.cs index a1e7eae5..77c6e355 100644 --- a/src/KeeperData.Core/Domain/Sites/Site.cs +++ b/src/KeeperData.Core/Domain/Sites/Site.cs @@ -22,6 +22,9 @@ public class Site : IAggregateRoot public string? ParentSiteIdentifier { get; private set; } public string? HoldingType { get; private set; } public string? PermanentLandHoldingIdentifier { get; private set; } + public DateTime? EffectiveFromDate { get; private set; } + public DateTime? EffectiveToDate { get; private set; } + public bool? ApprovalCurrentFlag { get; private set; } private readonly List _identifiers = []; public IReadOnlyCollection Identifiers => _identifiers.AsReadOnly(); @@ -60,7 +63,10 @@ public Site( Location? location, string? parentSiteIdentifier, string? holdingType, - string? permanentLandHoldingIdentifier) + string? permanentLandHoldingIdentifier, + DateTime? effectiveFromDate = null, + DateTime? effectiveToDate = null, + bool? approvalCurrentFlag = null) { Id = id; CreatedDate = createdDate; @@ -77,6 +83,9 @@ public Site( ParentSiteIdentifier = parentSiteIdentifier; HoldingType = holdingType; PermanentLandHoldingIdentifier = permanentLandHoldingIdentifier; + EffectiveFromDate = effectiveFromDate; + EffectiveToDate = effectiveToDate; + ApprovalCurrentFlag = approvalCurrentFlag; } public static Site Create( @@ -94,7 +103,10 @@ public static Site Create( string? holdingType, SiteType? type = null, Location? location = null, - string? permanentLandHoldingIdentifier = null) + string? permanentLandHoldingIdentifier = null, + DateTime? effectiveFromDate = null, + DateTime? effectiveToDate = null, + bool? approvalCurrentFlag = null) { var site = new Site( id, @@ -111,7 +123,10 @@ public static Site Create( location, parentSiteIdentifier, holdingType, - permanentLandHoldingIdentifier); + permanentLandHoldingIdentifier, + effectiveFromDate, + effectiveToDate, + approvalCurrentFlag); site._domainEvents.Add(new SiteCreatedDomainEvent(site.Id)); return site; @@ -128,7 +143,10 @@ public void Update( bool deleted, string? parentSiteIdentifier, string? holdingType, - string? permanentLandHoldingIdentifier) + string? permanentLandHoldingIdentifier, + DateTime? effectiveFromDate = null, + DateTime? effectiveToDate = null, + bool? approvalCurrentFlag = null) { var changed = false; @@ -142,6 +160,9 @@ public void Update( changed |= Change(ParentSiteIdentifier, parentSiteIdentifier, v => ParentSiteIdentifier = v, lastUpdatedDate); changed |= Change(HoldingType, holdingType, v => HoldingType = v, lastUpdatedDate); changed |= Change(PermanentLandHoldingIdentifier, permanentLandHoldingIdentifier, v => PermanentLandHoldingIdentifier = v, lastUpdatedDate); + changed |= Change(EffectiveFromDate, effectiveFromDate, v => EffectiveFromDate = v, lastUpdatedDate); + changed |= Change(EffectiveToDate, effectiveToDate, v => EffectiveToDate = v, lastUpdatedDate); + changed |= Change(ApprovalCurrentFlag, approvalCurrentFlag, v => ApprovalCurrentFlag = v, lastUpdatedDate); if (changed) { diff --git a/src/KeeperData.Infrastructure/ApiClients/DataBridgeClient.cs b/src/KeeperData.Infrastructure/ApiClients/DataBridgeClient.cs index 4339ff23..48298797 100644 --- a/src/KeeperData.Infrastructure/ApiClients/DataBridgeClient.cs +++ b/src/KeeperData.Infrastructure/ApiClients/DataBridgeClient.cs @@ -454,6 +454,45 @@ public async Task> GetSamCommonLandsByCommonCphAsync(string return result.Data; } + public async Task> GetSamShowgroundsByCphAsync(string cph, CancellationToken cancellationToken) + { + var query = DataBridgeQueries.SamShowgroundsByCph(cph); + var uri = UriTemplate.Resolve(DataBridgeApiRoutes.GetSamShowgrounds, new { }, query); + + var result = await GetFromApiAsync( + uri, + $"Sam showgrounds for CPH '{cph}'", + cancellationToken); + + return result.Data; + } + + public async Task?> GetSamShowgroundsAsync( + int top, + int skip, + string? selectFields = null, + DateTime? updatedSinceDateTime = null, + string? orderBy = null, + CancellationToken cancellationToken = default) + { + var _samShowgroundsEnabled = configuration.GetValue("DataBridgeCollectionFlags:SamShowgroundsEnabled"); + if (!_samShowgroundsEnabled) return null; + + return await ExecutePagedRequestWithMetricsAsync( + "sam_showgrounds", + top, + async () => + { + var query = DataBridgeQueries.PagedRecords(top, skip, selectFields, updatedSinceDateTime, orderBy); + var uri = UriTemplate.Resolve(DataBridgeApiRoutes.GetSamShowgrounds, new { }, query); + + return await GetFromApiAsync( + uri, + $"Sam paged showgrounds for top '{top}', skip '{skip}'", + cancellationToken); + }); + } + private async Task?> ExecutePagedRequestWithMetricsAsync( string collectionName, int batchSize, diff --git a/src/KeeperData.Infrastructure/ApiClients/Decorators/DataBridgeClientAnonymizer.cs b/src/KeeperData.Infrastructure/ApiClients/Decorators/DataBridgeClientAnonymizer.cs index 1bc838af..63214027 100644 --- a/src/KeeperData.Infrastructure/ApiClients/Decorators/DataBridgeClientAnonymizer.cs +++ b/src/KeeperData.Infrastructure/ApiClients/Decorators/DataBridgeClientAnonymizer.cs @@ -227,5 +227,22 @@ public async Task> GetSamCommonLandsByCommonCphAsync(string PiiAnonymizerHelper.AnonymizeAll(result, PiiAnonymizerHelper.AnonymizeSamCommonLand, logger); return result; } + public async Task?> GetSamShowgroundsAsync( + int top, + int skip, + string? selectFields = null, + DateTime? updatedSinceDateTime = null, + string? orderBy = null, + CancellationToken cancellationToken = default) + { + var result = await inner.GetSamShowgroundsAsync(top, skip, selectFields, updatedSinceDateTime, orderBy, cancellationToken); + PiiAnonymizerHelper.AnonymizeResponse(result, logger); + return result; + } + public async Task> GetSamShowgroundsByCphAsync(string cph, CancellationToken cancellationToken) + { + // No sensitive data here, so no anonymisation is needed. + return await inner.GetSamShowgroundsByCphAsync(cph, cancellationToken); + } } \ No newline at end of file diff --git a/src/KeeperData.Infrastructure/ApiClients/Fakes/FakeDataBridgeClient.cs b/src/KeeperData.Infrastructure/ApiClients/Fakes/FakeDataBridgeClient.cs index 1a33602f..8bccb2e3 100644 --- a/src/KeeperData.Infrastructure/ApiClients/Fakes/FakeDataBridgeClient.cs +++ b/src/KeeperData.Infrastructure/ApiClients/Fakes/FakeDataBridgeClient.cs @@ -254,6 +254,31 @@ public Task> GetSamCommonLandsByCommonCphAsync(string cph, C return Task.FromResult(GetSamCommonLands(commonCph: cph)); } + public Task?> GetSamShowgroundsAsync( + int top, + int skip, + string? selectFields = null, + DateTime? updatedSinceDateTime = null, + string? orderBy = null, + CancellationToken cancellationToken = default) + { + var data = Enumerable.Range(0, top).Select(_ => GetSamShowground()).SelectMany(x => x).ToList(); + + if (updatedSinceDateTime.HasValue) + { + data = data.Where(x => (x.UpdatedAtUtc >= updatedSinceDateTime) || (x.CreatedAtUtc >= updatedSinceDateTime)).ToList(); + } + + var objects = JsonSerializer.Deserialize>(JsonSerializer.Serialize(data)); + var response = GetDataBridgeResponse(objects!, top, skip); + return Task.FromResult?>(response); + } + + public Task> GetSamShowgroundsByCphAsync(string cph, CancellationToken cancellationToken) + { + return Task.FromResult(GetSamShowground(cph)); + } + private Task?> GenerateFakeCtsAgentOrKeeperResponseAsync(int top, int skip) { var data = Enumerable.Range(0, top).Select(_ => GetCtsAgentOrKeeper()).SelectMany(x => x).ToList(); @@ -463,4 +488,18 @@ private static List GetSamCommonLands(string? commonCph = null) CONTIGUOUS_COMMON = "No" }]; } + private List GetSamShowground(string? id = null) + { + return [ + new SamShowground { + BATCH_ID = 1, + CHANGE_TYPE = "I", + IsDeleted = false, + UpdatedAtUtc = DateTime.UtcNow, + CreatedAtUtc = DateTime.UtcNow, + CPH = id ?? $"{_random.Next(10, 99)}/{_random.Next(100, 999)}/{_random.Next(1000, 9999)}", + START_DATE = DateTime.Today.AddDays(-10), + END_DATE = null + }]; + } } \ No newline at end of file diff --git a/tests/KeeperData.Api.Tests.Component/Orchestration/Imports/Sam/Holdings/SamHoldingImportOrchestratorTests.cs b/tests/KeeperData.Api.Tests.Component/Orchestration/Imports/Sam/Holdings/SamHoldingImportOrchestratorTests.cs index 219e2094..3a99429e 100644 --- a/tests/KeeperData.Api.Tests.Component/Orchestration/Imports/Sam/Holdings/SamHoldingImportOrchestratorTests.cs +++ b/tests/KeeperData.Api.Tests.Component/Orchestration/Imports/Sam/Holdings/SamHoldingImportOrchestratorTests.cs @@ -47,7 +47,7 @@ public async Task GivenAHoldingIdentifier_WhenExecutingSamHoldingImportOrchestra var ports = new List(); var commonLands = new List(); - var (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri) = GetAllQueryUris(holdingIdentifier, parties.Select(x => x.PARTY_ID)); + var (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri, showgroundsUri) = GetAllQueryUris(holdingIdentifier, parties.Select(x => x.PARTY_ID)); SetupRepositoryMocks(); SetupLookupServiceMocks(); @@ -58,6 +58,7 @@ public async Task GivenAHoldingIdentifier_WhenExecutingSamHoldingImportOrchestra SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, partiesUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(parties)); SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, portsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(ports)); SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, commonLandsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(commonLands)); + SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, showgroundsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(new List())); var result = await ExecuteTestAsync(_appTestFixture.AppWebApplicationFactory, holdingIdentifier); @@ -115,7 +116,7 @@ public async Task GivenExistingRelationship_WhenHolderRemovesCph_ThenRelationshi var ports = new List(); var commonLands = new List(); - var (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri) = GetAllQueryUris(cph, []); + var (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri, showgroundsUri) = GetAllQueryUris(cph, []); SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, holdingsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(holdings)); SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, herdsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(herds)); @@ -123,6 +124,7 @@ public async Task GivenExistingRelationship_WhenHolderRemovesCph_ThenRelationshi SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, partiesUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(parties)); SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, portsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(ports)); SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, commonLandsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(commonLands)); + SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, showgroundsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(new List())); // Act await ExecuteTestAsync(_appTestFixture.AppWebApplicationFactory, cph); @@ -240,7 +242,7 @@ private static void VerifyGoldDataTypes(SamHoldingImportContext context, string } } - private static (string holdingsUri, string herdsUri, string holdersUri, string partiesUri, string commonLandsUri, string portsUri) GetAllQueryUris(string holdingIdentifier, IEnumerable partyIds) + private static (string holdingsUri, string herdsUri, string holdersUri, string partiesUri, string commonLandsUri, string portsUri, string showgroundsUri) GetAllQueryUris(string holdingIdentifier, IEnumerable partyIds) { var holdingsUri = RequestUriUtilities.GetQueryUri( DataBridgeApiRoutes.GetSamHoldings, @@ -272,7 +274,12 @@ private static (string holdingsUri, string herdsUri, string holdersUri, string p new { }, DataBridgeQueries.SamPortsByCph(holdingIdentifier)); - return (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri); + var showgroundsUri = RequestUriUtilities.GetQueryUri( + DataBridgeApiRoutes.GetSamShowgrounds, + new { }, + DataBridgeQueries.SamShowgroundsByCph(holdingIdentifier)); + + return (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri, showgroundsUri); } private void SetupRepositoryMocks( diff --git a/tests/KeeperData.Api.Tests.Component/Orchestration/Imports/Sam/SamBulkImportWithAccurateRawDataTests.cs b/tests/KeeperData.Api.Tests.Component/Orchestration/Imports/Sam/SamBulkImportWithAccurateRawDataTests.cs index c851133b..d9197b9d 100644 --- a/tests/KeeperData.Api.Tests.Component/Orchestration/Imports/Sam/SamBulkImportWithAccurateRawDataTests.cs +++ b/tests/KeeperData.Api.Tests.Component/Orchestration/Imports/Sam/SamBulkImportWithAccurateRawDataTests.cs @@ -55,7 +55,7 @@ private async Task RunDefaultScenarioAsync(SamTestScena { var partyIds = scenarioData.RawParties.Select(x => x.PARTY_ID).Union(scenarioData.RawHolders.Select(x => x.PARTY_ID)).Distinct().ToList(); - var (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri) = GetAllQueryUris(scenarioData.Cph, partyIds); + var (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri, showgroundsUri) = GetAllQueryUris(scenarioData.Cph, partyIds); SetupDefaultRepositoryMocks(); SetupDefaultLookupServiceMocks(); @@ -71,6 +71,7 @@ private async Task RunDefaultScenarioAsync(SamTestScena SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, partiesUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(scenarioData.RawParties)); SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, portsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(scenarioData.RawPorts ?? [])); SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, commonLandsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(scenarioData.RawCommonLandsByCommonCph)); + SetupDataBridgeApiRequest(_appTestFixture.AppWebApplicationFactory, showgroundsUri, HttpStatusCode.OK, HttpContentUtility.CreateResponseContentWithEnvelope(new List())); return await ExecuteTestAsync(_appTestFixture.AppWebApplicationFactory, scenarioData.Cph); } @@ -150,7 +151,7 @@ private static void SetupDataBridgeApiRequest(AppWebApplicationFactory factory, .ReturnsResponse(httpStatusCode, httpResponseMessage); } - private static (string holdingsUri, string herdsUri, string holdersUri, string partiesUri, string commonLandsUri, string portsUri) GetAllQueryUris(string holdingIdentifier, IEnumerable partyIds) + private static (string holdingsUri, string herdsUri, string holdersUri, string partiesUri, string commonLandsUri, string portsUri, string showgroundsUri) GetAllQueryUris(string holdingIdentifier, IEnumerable partyIds) { var holdingsUri = RequestUriUtilities.GetQueryUri( DataBridgeApiRoutes.GetSamHoldings, @@ -182,7 +183,12 @@ private static (string holdingsUri, string herdsUri, string holdersUri, string p new { }, DataBridgeQueries.SamCommonLandsByCommonCph(holdingIdentifier)); - return (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri); + var showgroundsUri = RequestUriUtilities.GetQueryUri( + DataBridgeApiRoutes.GetSamShowgrounds, + new { }, + DataBridgeQueries.SamShowgroundsByCph(holdingIdentifier)); + + return (holdingsUri, herdsUri, holdersUri, partiesUri, commonLandsUri, portsUri, showgroundsUri); } private void SetupDefaultRepositoryMocks() diff --git a/tests/KeeperData.Application.Tests.Unit/Orchestration/ChangeScanning/Sam/SamShowgroundBulkScanStepTests.cs b/tests/KeeperData.Application.Tests.Unit/Orchestration/ChangeScanning/Sam/SamShowgroundBulkScanStepTests.cs new file mode 100644 index 00000000..ad87912a --- /dev/null +++ b/tests/KeeperData.Application.Tests.Unit/Orchestration/ChangeScanning/Sam/SamShowgroundBulkScanStepTests.cs @@ -0,0 +1,75 @@ +using FluentAssertions; +using KeeperData.Application.Orchestration.ChangeScanning.Sam.Bulk; +using KeeperData.Application.Orchestration.ChangeScanning.Sam.Bulk.Steps; +using KeeperData.Core.ApiClients.DataBridgeApi; +using KeeperData.Core.ApiClients.DataBridgeApi.Configuration; +using KeeperData.Core.ApiClients.DataBridgeApi.Contracts; +using KeeperData.Core.Messaging.Contracts.V1.Sam; +using KeeperData.Core.Messaging.MessagePublishers; +using KeeperData.Core.Messaging.MessagePublishers.Clients; +using KeeperData.Core.Providers; +using Microsoft.Extensions.Logging; +using Moq; + +namespace KeeperData.Application.Tests.Unit.Orchestration.ChangeScanning.Sam; + +public class SamShowgroundBulkScanStepTests +{ + private readonly Mock _dataBridgeClientMock = new(); + private readonly Mock> _messagePublisherMock = new(); + private readonly Mock> _loggerMock = new(); + private readonly DataBridgeScanConfiguration _config = new() { QueryPageSize = 5, DelayBetweenQueriesSeconds = 0 }; + private readonly Mock _delayProviderMock = new(); + + private readonly SamShowgroundBulkScanStep _scanStep; + private readonly SamBulkScanContext _context; + + public SamShowgroundBulkScanStepTests() + { + _scanStep = new SamShowgroundBulkScanStep( + _dataBridgeClientMock.Object, + _messagePublisherMock.Object, + _config, + _delayProviderMock.Object, + _loggerMock.Object); + + _context = new SamBulkScanContext + { + Holdings = new(), + Showgrounds = new() + }; + } + + [Fact] + public async Task ExecuteCoreAsync_ShouldPublishMessages_AndUpdateContext_WhenDataReturned() + { + var responseMock = new DataBridgeResponse + { + CollectionName = "collection", + Count = 2, + TotalCount = 2, + Top = 5, + Skip = 0, + Data = [ + new SamScanShowgroundIdentifier { CPH = "12/345/6789" }, + new SamScanShowgroundIdentifier { CPH = "98/765/4321" } + ] + }; + + _dataBridgeClientMock + .Setup(c => c.GetSamShowgroundsAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(responseMock); + + await _scanStep.ExecuteAsync(_context, CancellationToken.None); + + _messagePublisherMock.Verify(p => p.PublishAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _context.Showgrounds.CurrentSkip.Should().Be(2); + _context.Showgrounds.ScanCompleted.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/tests/KeeperData.Application.Tests.Unit/Orchestration/ChangeScanning/Sam/SamShowgroundDailyScanStepTests.cs b/tests/KeeperData.Application.Tests.Unit/Orchestration/ChangeScanning/Sam/SamShowgroundDailyScanStepTests.cs new file mode 100644 index 00000000..a1cafda8 --- /dev/null +++ b/tests/KeeperData.Application.Tests.Unit/Orchestration/ChangeScanning/Sam/SamShowgroundDailyScanStepTests.cs @@ -0,0 +1,98 @@ +using FluentAssertions; +using KeeperData.Application.Orchestration.ChangeScanning.Sam.Daily; +using KeeperData.Application.Orchestration.ChangeScanning.Sam.Daily.Steps; +using KeeperData.Core.ApiClients.DataBridgeApi; +using KeeperData.Core.ApiClients.DataBridgeApi.Configuration; +using KeeperData.Core.ApiClients.DataBridgeApi.Contracts; +using KeeperData.Core.Exceptions; +using KeeperData.Core.Messaging.Contracts.V1.Sam; +using KeeperData.Core.Messaging.MessagePublishers; +using KeeperData.Core.Messaging.MessagePublishers.Clients; +using KeeperData.Core.Providers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; + +namespace KeeperData.Application.Tests.Unit.Orchestration.ChangeScanning.Sam; + +public class SamShowgroundDailyScanStepTests +{ + private readonly Mock _dataBridgeClientMock = new(); + private readonly Mock> _messagePublisherMock = new(); + private readonly Mock> _loggerMock = new(); + private readonly DataBridgeScanConfiguration _config = new() { QueryPageSize = 5, DelayBetweenQueriesSeconds = 0 }; + private readonly Mock _delayProviderMock = new(); + private readonly IConfiguration _configuration; + + private readonly SamShowgroundDailyScanStep _scanStep; + private readonly SamDailyScanContext _context; + + public SamShowgroundDailyScanStepTests() + { + _configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "DataBridgeCollectionFlags:SamShowgroundsEnabled", "true" } }) + .Build(); + + _scanStep = new SamShowgroundDailyScanStep( + _dataBridgeClientMock.Object, + _messagePublisherMock.Object, + _config, + _delayProviderMock.Object, + _configuration, + _loggerMock.Object); + + _context = new SamDailyScanContext + { + CurrentDateTime = DateTime.UtcNow, + UpdatedSinceDateTime = DateTime.UtcNow.AddHours(-24), + Showgrounds = new() + }; + } + + [Fact] + public async Task ExecuteCoreAsync_ShouldExitWhenSamShowgroundsDisabled() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "DataBridgeCollectionFlags:SamShowgroundsEnabled", "false" } }) + .Build(); + + var scanStep = new SamShowgroundDailyScanStep( + _dataBridgeClientMock.Object, + _messagePublisherMock.Object, + _config, + _delayProviderMock.Object, + configuration, + _loggerMock.Object); + + await scanStep.ExecuteAsync(_context, CancellationToken.None); + + _dataBridgeClientMock.Verify(c => c.GetSamShowgroundsAsync( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task ExecuteCoreAsync_ShouldPublishSamUpdateHoldingMessage() + { + var expectedCph = "12/345/6789"; + var responseMock = new DataBridgeResponse + { + CollectionName = "collection", + Top = 1, + Skip = 0, + Count = 1, + TotalCount = 1, + Data = [new SamScanShowgroundIdentifier { CPH = expectedCph }] + }; + + _dataBridgeClientMock + .Setup(c => c.GetSamShowgroundsAsync(5, 0, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(responseMock); + + await _scanStep.ExecuteAsync(_context, CancellationToken.None); + + _messagePublisherMock.Verify(p => p.PublishAsync( + It.Is(m => m.Identifier == expectedCph), + It.IsAny()), Times.Once); + } +} \ No newline at end of file diff --git a/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamHoldingMapperTests.cs b/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamHoldingMapperTests.cs index 9a03a72e..36b6fddd 100644 --- a/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamHoldingMapperTests.cs +++ b/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamHoldingMapperTests.cs @@ -125,6 +125,7 @@ public async Task ToGold_WithPermanentLandHoldingRelationship_ShouldSetPermanent [silverHolding], [], [], + [], (_, _) => Task.FromResult(null), (_, _) => Task.FromResult(null), (code, _) => Task.FromResult( @@ -168,6 +169,7 @@ public async Task ToGold_WithNonPermanentLandHoldingRelationship_ShouldSetParent [silverHolding], [], [], + [], (_, _) => Task.FromResult(null), (_, _) => Task.FromResult(null), (code, _) => Task.FromResult( @@ -211,6 +213,7 @@ public async Task ToGold_WithNullSecondaryCph_ShouldHaveNullIdentifiers() [silverHolding], [], [], + [], (_, _) => Task.FromResult(null), (_, _) => Task.FromResult(null), (code, _) => Task.FromResult( @@ -277,6 +280,7 @@ public async Task ToGold_PrefersActiveSamHolding_OverCommonLand() [activeSamHolding, activeCommonLand], [], [], + [], (_, _) => Task.FromResult(null), (_, _) => Task.FromResult(null), (code, _) => Task.FromResult( @@ -329,6 +333,7 @@ public async Task ToGold_PrefersInactiveSamHolding_OverActiveCommonLand() [inactiveSamHolding, activeCommonLand], [], [], + [], (_, _) => Task.FromResult(null), (_, _) => Task.FromResult(null), (code, _) => Task.FromResult( @@ -372,6 +377,7 @@ public async Task ToGold_SelectsCommonLand_WhenOnlyCommonLandPresent() [activeCommonLand], [], [], + [], (_, _) => Task.FromResult(null), (_, _) => Task.FromResult(null), (code, _) => Task.FromResult( @@ -423,6 +429,7 @@ public async Task ToGold_SelectsMostRecentActiveSamHolding_WhenMultipleActiveSam [olderSamHolding, newerSamHolding], [], [], + [], (_, _) => Task.FromResult(null), (_, _) => Task.FromResult(null), (code, _) => Task.FromResult( @@ -440,4 +447,90 @@ public async Task ToGold_SelectsMostRecentActiveSamHolding_WhenMultipleActiveSam result.Species.Should().Contain(s => s.Code == "SHE"); result.Species.Should().Contain(s => s.Code == "PIG"); } + + [Theory] + [InlineData(null, null, true)] // No effective dates -> always current + [InlineData(-5, null, true)] // Started in the past, no end -> current + [InlineData(5, null, false)] // Starts in the future -> not current + [InlineData(null, 5, true)] // Ends in the future, no start -> current + [InlineData(null, -5, false)] // Ended in the past -> not current + [InlineData(-5, 5, true)] // Within the effective window -> current + [InlineData(5, 10, false)] // Window entirely in the future -> not current + [InlineData(-10, -5, false)] // Window entirely in the past -> not current + public async Task ToGold_WithShowground_SetsApprovalCurrentFlagFromEffectiveDates( + int? fromOffsetDays, int? toOffsetDays, bool expectedFlag) + { + var now = DateTime.UtcNow; + var showground = new SamShowground + { + CPH = "12/345/6789", + START_DATE = fromOffsetDays.HasValue ? now.AddDays(fromOffsetDays.Value) : null, + END_DATE = toOffsetDays.HasValue ? now.AddDays(toOffsetDays.Value) : null + }; + + var result = await SamHoldingMapper.ToGold( + "gold-site-id", + null, + [BuildSilverHolding()], + [], + [], + [showground], + (_, _) => Task.FromResult(null), + (_, _) => Task.FromResult(null), + (code, _) => Task.FromResult(BuildCphnIdentifierType(code)), + (_, _) => Task.FromResult<(string?, string?)>((null, null)), + (_, _) => Task.FromResult(null), + Mock.Of(), + CancellationToken.None); + + result.Should().NotBeNull(); + result!.ApprovalCurrentFlag.Should().Be(expectedFlag); + result.EffectiveFromDate.Should().Be(showground.START_DATE); + result.EffectiveToDate.Should().Be(showground.END_DATE); + } + + [Fact] + public async Task ToGold_WithoutShowground_LeavesApprovalFieldsNull() + { + var result = await SamHoldingMapper.ToGold( + "gold-site-id", + null, + [BuildSilverHolding()], + [], + [], + [], + (_, _) => Task.FromResult(null), + (_, _) => Task.FromResult(null), + (code, _) => Task.FromResult(BuildCphnIdentifierType(code)), + (_, _) => Task.FromResult<(string?, string?)>((null, null)), + (_, _) => Task.FromResult(null), + Mock.Of(), + CancellationToken.None); + + result.Should().NotBeNull(); + result!.ApprovalCurrentFlag.Should().BeNull(); + result.EffectiveFromDate.Should().BeNull(); + result.EffectiveToDate.Should().BeNull(); + } + + private static SamHoldingDocument BuildSilverHolding() => new() + { + CountyParishHoldingNumber = "12/345/6789", + LocationName = "Test Farm", + HoldingStatus = "Active", + CreatedDate = DateTime.UtcNow, + LastUpdatedDate = DateTime.UtcNow, + HoldingStartDate = DateTime.UtcNow + }; + + private static SiteIdentifierTypeDocument? BuildCphnIdentifierType(string? code) => + code == "CPHN" + ? new SiteIdentifierTypeDocument + { + IdentifierId = "type-id", + Code = "CPHN", + Name = "CPH Number", + LastModifiedDate = DateTime.UtcNow + } + : null; } \ No newline at end of file diff --git a/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamHoldingMapperToGoldTests.cs b/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamHoldingMapperToGoldTests.cs index 73344442..30a91d1b 100644 --- a/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamHoldingMapperToGoldTests.cs +++ b/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamHoldingMapperToGoldTests.cs @@ -1,6 +1,7 @@ using FluentAssertions; using KeeperData.Application.Orchestration.Imports.Sam.Mappings; using KeeperData.Application.Services; +using KeeperData.Core.ApiClients.DataBridgeApi.Contracts; using KeeperData.Core.Documents; using KeeperData.Core.Documents.Silver; using KeeperData.Core.Services; @@ -261,6 +262,7 @@ public async Task WhenMappingEmptyListOfSamHoldingDocument() new List() { }, new List(), new List(), + new List(), _getCountryById, _getSiteTypeByCode, _getSiteIdentifierTypeByCode, @@ -787,6 +789,30 @@ public async Task WhenUpdatingSiteTypeSite_ItShouldUpdate() result.Type.Name.Should().Be("prem2name"); } + [Fact] + public async Task WhenMappingShowgroundData_ShouldMapEffectiveDatesCorrectly() + { + // Arrange + var inputHolding = new SamHoldingDocument { CountyParishHoldingNumber = "12/345/6789" }; + var rawShowgrounds = new List + { + new SamShowground + { + CPH = "12/345/6789", + START_DATE = DateTime.UtcNow.AddDays(-10), + END_DATE = DateTime.UtcNow.AddDays(10) + } + }; + + // Act + var result = await WhenIMapSilverSiteToGold(inputHolding, null, null, rawShowgrounds); + + // Assert + result!.EffectiveFromDate.Should().Be(rawShowgrounds[0].START_DATE); + result.EffectiveToDate.Should().Be(rawShowgrounds[0].END_DATE); + result.ApprovalCurrentFlag.Should().BeTrue(); + } + [Fact] public void SelectAddressSource_WhenCommonLandAndSiteHoldingPresent_ShouldReturnCommonLand() { @@ -988,6 +1014,7 @@ private static SiteDocument GetBlankSiteDocument() Name = "", Source = "SAM", HoldingType = string.Empty, + ApprovalCurrentFlag = null, Location = new LocationDocument() { IdentifierId = "any-guid", @@ -1023,12 +1050,12 @@ private static SiteDocument GetBlankSiteDocument() }; } - private async Task WhenIMapSilverSiteToGold(SamHoldingDocument inputHolding, SiteDocument? existingSite, List? goldSiteGroupMarks = null) + private async Task WhenIMapSilverSiteToGold(SamHoldingDocument inputHolding, SiteDocument? existingSite, List? goldSiteGroupMarks = null, List? rawShowgrounds = null) { - return await WhenIMapSilverSitesToGold(new List() { inputHolding }, existingSite, goldSiteGroupMarks); + return await WhenIMapSilverSitesToGold(new List() { inputHolding }, existingSite, goldSiteGroupMarks, rawShowgrounds); } - private async Task WhenIMapSilverSitesToGold(List inputHoldings, SiteDocument? existingSite, List? goldSiteGroupMarks = null) + private async Task WhenIMapSilverSitesToGold(List inputHoldings, SiteDocument? existingSite, List? goldSiteGroupMarks = null, List? rawShowgrounds = null) { return await SamHoldingMapper.ToGold( GoldSiteId, @@ -1036,6 +1063,7 @@ private static SiteDocument GetBlankSiteDocument() inputHoldings, goldSiteGroupMarks ?? [], new List(), + rawShowgrounds ?? [], _getCountryById, _getSiteTypeByCode, _getSiteIdentifierTypeByCode, diff --git a/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Steps/SamHoldingImportAggregationStepTests.cs b/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Steps/SamHoldingImportAggregationStepTests.cs index 5c540d53..02809af4 100644 --- a/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Steps/SamHoldingImportAggregationStepTests.cs +++ b/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Steps/SamHoldingImportAggregationStepTests.cs @@ -28,10 +28,12 @@ public async Task ExecuteCoreAsync_AggregatesData() var herd = new SamHerd { OWNER_PARTY_IDS = "P1", KEEPER_PARTY_IDS = "P2" }; var party1 = new SamParty { PARTY_ID = "P1" }; var party2 = new SamParty { PARTY_ID = "P2" }; + var showground = new SamShowground { CPH = "12/345/6789" }; _clientMock.Setup(x => x.GetSamHoldingsAsync(context.Cph, It.IsAny())).ReturnsAsync([holding]); _clientMock.Setup(x => x.GetSamHoldersByCphAsync(context.Cph, It.IsAny())).ReturnsAsync([holder]); _clientMock.Setup(x => x.GetSamHerdsAsync(context.Cph, It.IsAny())).ReturnsAsync([herd]); + _clientMock.Setup(x => x.GetSamShowgroundsByCphAsync(context.Cph, It.IsAny())).ReturnsAsync([showground]); _clientMock.Setup(x => x.GetSamPartiesAsync( It.Is>(ids => ids.Contains("P1") && ids.Contains("P2")), @@ -44,6 +46,7 @@ public async Task ExecuteCoreAsync_AggregatesData() context.RawHolders.Should().Contain(holder); context.RawHerds.Should().Contain(herd); context.RawParties.Should().HaveCount(2); + context.RawShowgrounds.Should().Contain(showground); } [Fact] diff --git a/tests/KeeperData.Application.Tests.Unit/Queries/Pagination/PaginatedResultTests.cs b/tests/KeeperData.Application.Tests.Unit/Queries/Pagination/PaginatedResultTests.cs new file mode 100644 index 00000000..c25ff556 --- /dev/null +++ b/tests/KeeperData.Application.Tests.Unit/Queries/Pagination/PaginatedResultTests.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using KeeperData.Application.Queries.Pagination; + +namespace KeeperData.Application.Tests.Unit.Queries.Pagination; + +public class PaginatedResultTests +{ + [Fact] + public void Map_ProjectsValues_AndPreservesPaginationMetadata() + { + var source = new PaginatedResult + { + Count = 2, + TotalCount = 10, + Values = [1, 2], + Page = 2, + PageSize = 2, + NextCursor = "cursor-123" + }; + + var mapped = source.Map(x => x.ToString()); + + mapped.Values.Should().Equal("1", "2"); + mapped.Count.Should().Be(2); + mapped.TotalCount.Should().Be(10); + mapped.Page.Should().Be(2); + mapped.PageSize.Should().Be(2); + mapped.NextCursor.Should().Be("cursor-123"); + } + + [Fact] + public void Map_WithEmptyValues_ReturnsEmptyResult() + { + var source = new PaginatedResult + { + Count = 0, + TotalCount = 0, + Values = [], + Page = 1, + PageSize = 25 + }; + + var mapped = source.Map(x => x * 2); + + mapped.Values.Should().BeEmpty(); + mapped.NextCursor.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/KeeperData.Application.Tests.Unit/Services/SiteActivityTypeLookupServiceTests.cs b/tests/KeeperData.Application.Tests.Unit/Services/SiteActivityTypeLookupServiceTests.cs index 2e6c2ce0..369a50a4 100644 --- a/tests/KeeperData.Application.Tests.Unit/Services/SiteActivityTypeLookupServiceTests.cs +++ b/tests/KeeperData.Application.Tests.Unit/Services/SiteActivityTypeLookupServiceTests.cs @@ -115,4 +115,26 @@ public async Task FindAsync_WhenNotFound_ReturnsNullTuple() result.siteActivityTypeId.Should().BeNull(); result.siteActivityTypeName.Should().BeNull(); } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetByIdAsync_WhenIdMissing_ReturnsNull(string? id) + { + var result = await _sut.GetByIdAsync(id, CancellationToken.None); + + result.Should().BeNull(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetByCodeAsync_WhenCodeMissing_ReturnsNull(string? code) + { + var result = await _sut.GetByCodeAsync(code, CancellationToken.None); + + result.Should().BeNull(); + } } \ No newline at end of file diff --git a/tests/KeeperData.Application.Tests.Unit/Services/SiteTypeLookupServiceTests.cs b/tests/KeeperData.Application.Tests.Unit/Services/SiteTypeLookupServiceTests.cs index b9456917..e0ea3ba6 100644 --- a/tests/KeeperData.Application.Tests.Unit/Services/SiteTypeLookupServiceTests.cs +++ b/tests/KeeperData.Application.Tests.Unit/Services/SiteTypeLookupServiceTests.cs @@ -115,4 +115,68 @@ public async Task FindAsync_WhenNotFound_ReturnsNullTuple() result.siteTypeId.Should().BeNull(); result.siteTypeName.Should().BeNull(); } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetByIdAsync_WhenIdMissing_ReturnsNull(string? id) + { + var result = await _sut.GetByIdAsync(id, CancellationToken.None); + + result.Should().BeNull(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetByCodeAsync_WhenCodeMissing_ReturnsNull(string? code) + { + var result = await _sut.GetByCodeAsync(code, CancellationToken.None); + + result.Should().BeNull(); + } + + [Fact] + public async Task GetByCodeAsync_WhenMatchExists_ReturnsSiteType() + { + var doc = new SiteTypeDocument + { + IdentifierId = "AC", + Code = "AC", + Name = "Assembly Centre", + IsActive = true, + SortOrder = 0, + EffectiveStartDate = DateTime.UtcNow, + CreatedDate = DateTime.UtcNow + }; + _mockCache.Setup(c => c.SiteTypes).Returns(new[] { doc }); + + var result = await _sut.GetByCodeAsync("ac", CancellationToken.None); + + result.Should().Be(doc); + } + + [Fact] + public async Task GetByCodeAsync_WhenNotFound_ReturnsNull() + { + _mockCache.Setup(c => c.SiteTypes).Returns(Array.Empty()); + + var result = await _sut.GetByCodeAsync("missing", CancellationToken.None); + + result.Should().BeNull(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task FindAsync_WhenLookupValueMissing_ReturnsNullTuple(string? lookupValue) + { + var result = await _sut.FindAsync(lookupValue, CancellationToken.None); + + result.siteTypeId.Should().BeNull(); + result.siteTypeName.Should().BeNull(); + } } \ No newline at end of file diff --git a/tests/KeeperData.Core.Tests.Unit/Documents/SiteDocumentExtensionsTests.cs b/tests/KeeperData.Core.Tests.Unit/Documents/SiteDocumentExtensionsTests.cs index 74136dae..a67fe766 100644 --- a/tests/KeeperData.Core.Tests.Unit/Documents/SiteDocumentExtensionsTests.cs +++ b/tests/KeeperData.Core.Tests.Unit/Documents/SiteDocumentExtensionsTests.cs @@ -32,6 +32,62 @@ public void ToDto_WithFullSiteDocument_ShouldMapAllProperties() result.Species.Should().HaveCount(1); result.Marks.Should().HaveCount(1); result.Activities.Should().HaveCount(1); + result.EffectiveFromDate.Should().Be(siteDocument.EffectiveFromDate); + result.EffectiveToDate.Should().Be(siteDocument.EffectiveToDate); + result.ApprovalCurrentFlag.Should().Be(siteDocument.ApprovalCurrentFlag); + } + + [Fact] + public void ToDto_WithAssociatedHoldings_ShouldMapMainAndCommonLandHoldings() + { + // Arrange + var siteDocument = new SiteDocument + { + Id = "site-123", + Name = "Test Site", + StartDate = DateTime.UtcNow, + LastUpdatedDate = DateTime.UtcNow, + Identifiers = [], + Parties = [], + Species = [], + Marks = [], + Activities = [], + AssociatedMainHoldings = + [ + new AssociatedHoldingDocument + { + HoldingIdentifier = "12/345/6789", + ContiguousFlag = true, + StartDate = "2023-01-01", + EndDate = "2024-01-01" + } + ], + AssociatedCommonLands = + [ + new AssociatedHoldingDocument + { + HoldingIdentifier = "98/765/4321", + ContiguousFlag = false, + StartDate = "2022-06-01", + EndDate = null + } + ] + }; + + // Act + var result = siteDocument.ToDto(); + + // Assert + result.AssociatedMainHoldings.Should().ContainSingle(); + result.AssociatedMainHoldings[0].HoldingIdentifier.Should().Be("12/345/6789"); + result.AssociatedMainHoldings[0].ContiguousFlag.Should().BeTrue(); + result.AssociatedMainHoldings[0].StartDate.Should().Be("2023-01-01"); + result.AssociatedMainHoldings[0].EndDate.Should().Be("2024-01-01"); + + result.AssociatedCommonLands.Should().ContainSingle(); + result.AssociatedCommonLands[0].HoldingIdentifier.Should().Be("98/765/4321"); + result.AssociatedCommonLands[0].ContiguousFlag.Should().BeFalse(); + result.AssociatedCommonLands[0].EndDate.Should().BeNull(); } [Fact] @@ -865,7 +921,10 @@ private static SiteDocument CreateFullSiteDocument() }, StartDate = DateTime.UtcNow } - ] + ], + EffectiveFromDate = new DateTime(2023, 1, 1), + EffectiveToDate = new DateTime(2024, 1, 1), + ApprovalCurrentFlag = false }; } } \ No newline at end of file diff --git a/tests/KeeperData.Core.Tests.Unit/Domain/Sites/SiteDocumentTests.cs b/tests/KeeperData.Core.Tests.Unit/Domain/Sites/SiteDocumentTests.cs index 327e135c..dee47f9c 100644 --- a/tests/KeeperData.Core.Tests.Unit/Domain/Sites/SiteDocumentTests.cs +++ b/tests/KeeperData.Core.Tests.Unit/Domain/Sites/SiteDocumentTests.cs @@ -10,7 +10,7 @@ public class SiteDocumentTests public void WhenSiteIsEmpty_ToDomainShouldMapCorrectly() { var sut = new SiteDocument() { Id = "", Name = "" }; - var expected = new Site("", DateTime.MinValue, DateTime.MinValue, "", DateTime.MinValue, null, null, null, null, false, null, null, null, null, null); + var expected = new Site("", DateTime.MinValue, DateTime.MinValue, "", DateTime.MinValue, null, null, null, null, false, null, null, null, null, null, null, null); var result = sut.ToDomain(); @@ -141,7 +141,7 @@ private static SiteActivityDocument MakeSiteActivityDocument(string id, string p private static Site EmptySite(DateTime? lastUpdatedDate = null) { lastUpdatedDate ??= DateTime.MinValue; - return new Site("", DateTime.MinValue, lastUpdatedDate!.Value, "", DateTime.MinValue, null, null, null, null, false, null, null, null, null, null); + return new Site("", DateTime.MinValue, lastUpdatedDate!.Value, "", DateTime.MinValue, null, null, null, null, false, null, null, null, null, null, null, null); } private static SiteDocument EmptySiteDocument() diff --git a/tests/KeeperData.Core.Tests.Unit/Domain/Sites/SiteTests.cs b/tests/KeeperData.Core.Tests.Unit/Domain/Sites/SiteTests.cs index 923a7d87..13831aa7 100644 --- a/tests/KeeperData.Core.Tests.Unit/Domain/Sites/SiteTests.cs +++ b/tests/KeeperData.Core.Tests.Unit/Domain/Sites/SiteTests.cs @@ -364,6 +364,6 @@ private static SiteActivity CreateSiteActivity(string actId) private static Site CreateSite(DateTime lastUpdatedDate, Location? location = null) { - return new Site("id", DateTime.MinValue, lastUpdatedDate, "site-name", DateTime.MinValue, null, null, null, null, false, null, location, null, null, null); + return new Site("id", DateTime.MinValue, lastUpdatedDate, "site-name", DateTime.MinValue, null, null, null, null, false, null, location, null, null, null, null, null, null); } } \ No newline at end of file