diff --git a/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportSilverMappingStep.cs b/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportSilverMappingStep.cs index 741c0f34..616c8257 100644 --- a/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportSilverMappingStep.cs +++ b/src/KeeperData.Application/Orchestration/Imports/Sam/Holdings/Steps/SamHoldingImportSilverMappingStep.cs @@ -25,7 +25,10 @@ protected override async Task ExecuteCoreAsync(SamHoldingImportContext context, countryIdentifierLookupService.FindAsync, cancellationToken); - var commonLandHoldings = SamCommonLandMapper.ToSilver(context.RawCommonLandsByCommonCph); + var commonLandHoldings = await SamCommonLandMapper.ToSilver( + context.RawCommonLandsByCommonCph, + countryIdentifierLookupService.FindAsync, + cancellationToken); if (commonLandHoldings.Count > 0) { context.SilverHoldings.AddRange(commonLandHoldings); diff --git a/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamCommonLandMapper.cs b/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamCommonLandMapper.cs index 0e7f46b0..a63632f9 100644 --- a/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamCommonLandMapper.cs +++ b/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamCommonLandMapper.cs @@ -10,7 +10,10 @@ public static class SamCommonLandMapper private const string CommonLandSiteTypeCode = "CL"; private const string CommonLandBusinessUsage = "Common Land"; - public static List ToSilver(List rawCommonLands) + public static async Task> ToSilver( + List rawCommonLands, + Func> resolveCountry, + CancellationToken cancellationToken) { if (rawCommonLands == null || rawCommonLands.Count == 0) return []; @@ -33,6 +36,8 @@ public static List ToSilver(List rawCommonLan }); } + var (countryId, countryCode, _) = await resolveCountry(representative.COUNTRY, null, cancellationToken); + var holding = new SamHoldingDocument { LastUpdatedBatchId = representative.BATCH_ID, @@ -64,10 +69,11 @@ public static List ToSilver(List rawCommonLan { IdentifierId = Guid.NewGuid().ToString(), AddressLine = representative.ADDRESS_LINE_1, - AddressLocality = representative.ADDRESS_LINE_2, - AddressStreet = representative.ADDRESS_LINE_3, + AddressStreet = representative.ADDRESS_LINE_2, + AddressTown = representative.ADDRESS_LINE_3, AddressPostCode = representative.POSTCODE, - CountryCode = representative.COUNTRY + CountryCode = countryCode, + CountryIdentifier = countryId } } }; diff --git a/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamHoldingMapper.cs b/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamHoldingMapper.cs index bb6b60c5..ac504b9b 100644 --- a/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamHoldingMapper.cs +++ b/src/KeeperData.Application/Orchestration/Imports/Sam/Mappings/SamHoldingMapper.cs @@ -165,6 +165,19 @@ internal static SamHoldingDocument SelectRepresentativeHolding(List h.LastUpdatedDate).First(); } + public static SamHoldingDocument SelectAddressSource(List silverHoldings) + { + const string commonLandBusinessUsage = "Common Land"; + + // Common land address takes precedence — use the most recently updated common land if present + var commonLand = silverHoldings + .Where(x => x.SourceFacilitySubBusinessActivityCode == commonLandBusinessUsage) + .OrderByDescending(h => h.LastUpdatedDate) + .FirstOrDefault(); + + return commonLand ?? SelectRepresentativeHolding(silverHoldings); + } + public static async Task ToGold( string goldSiteId, SiteDocument? existingSite, @@ -185,6 +198,9 @@ internal static SamHoldingDocument SelectRepresentativeHolding(List h.SpeciesTypeCode), findSpecies, @@ -220,6 +236,7 @@ internal static SamHoldingDocument SelectRepresentativeHolding(List activities, string private static async Task CreateSiteAsync( string goldSiteId, SamHoldingDocument representative, + SamHoldingDocument addressSource, List goldSiteGroupMarks, List goldParties, Func> getCountryById, @@ -350,13 +369,13 @@ private static async Task CreateSiteAsync( SiteIdentifierType? siteIdentifierType, CancellationToken cancellationToken) { - var (address, communication) = await ResolveLocationPartsAsync(representative, getCountryById, cancellationToken); + var (address, communication) = await ResolveLocationPartsAsync(addressSource, getCountryById, cancellationToken); var isPermanentLandHolding = representative.CphRelationshipType.IsPermanentLandHolding(); var location = Location.Create( - representative.Location?.OsMapReference, - representative.Location?.Easting, - representative.Location?.Northing, + addressSource.Location?.OsMapReference, + addressSource.Location?.Easting, + addressSource.Location?.Northing, address, communication: [communication]); @@ -384,6 +403,7 @@ private static async Task CreateSiteAsync( private static async Task UpdateSiteAsync( SamHoldingDocument representative, + SamHoldingDocument addressSource, SiteDocument existing, List goldSiteGroupMarks, List goldParties, @@ -410,16 +430,16 @@ private static async Task UpdateSiteAsync( representative.CphTypeIdentifier, isPermanentLandHolding ? representative.SecondaryCph : null); - var (updatedAddress, updatedCommunication) = await ResolveLocationPartsAsync(representative, getCountryById, cancellationToken); + var (updatedAddress, updatedCommunication) = await ResolveLocationPartsAsync(addressSource, getCountryById, cancellationToken); // Always set the derived site type (may be null if no mapping found). site.SetSiteType(siteType, representative.LastUpdatedDate); site.SetLocation( representative.LastUpdatedDate, - representative.Location?.OsMapReference, - representative.Location?.Easting, - representative.Location?.Northing, + addressSource.Location?.OsMapReference, + addressSource.Location?.Easting, + addressSource.Location?.Northing, updatedAddress, [updatedCommunication]); diff --git a/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamCommonLandMapperTests.cs b/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamCommonLandMapperTests.cs index 9cc2604e..16c5d33f 100644 --- a/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamCommonLandMapperTests.cs +++ b/tests/KeeperData.Application.Tests.Unit/Orchestration/Imports/Sam/Mappings/SamCommonLandMapperTests.cs @@ -9,34 +9,40 @@ namespace KeeperData.Application.Tests.Unit.Orchestration.Imports.Sam.Mappings; public class SamCommonLandMapperTests { + private static readonly Func> NoopResolveCountry = + (_, _, _) => Task.FromResult<(string?, string?, string?)>((null, null, null)); + + private static Func> ResolveCountry(string? id, string? code) => + (_, _, _) => Task.FromResult<(string?, string?, string?)>((id, code, null)); + [Fact] - public void ToSilver_WithNullInput_ShouldReturnEmptyList() + public async Task ToSilver_WithNullInput_ShouldReturnEmptyList() { // Arrange List? rawCommonLands = null; // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands!); + var result = await SamCommonLandMapper.ToSilver(rawCommonLands!, NoopResolveCountry, CancellationToken.None); // Assert result.Should().BeEmpty(); } [Fact] - public void ToSilver_WithEmptyList_ShouldReturnEmptyList() + public async Task ToSilver_WithEmptyList_ShouldReturnEmptyList() { // Arrange var rawCommonLands = new List(); // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands); + var result = await SamCommonLandMapper.ToSilver(rawCommonLands, NoopResolveCountry, CancellationToken.None); // Assert result.Should().BeEmpty(); } [Fact] - public void ToSilver_WithDefinitionRecordOnly_ShouldCreateSamHoldingDocument() + public async Task ToSilver_WithDefinitionRecordOnly_ShouldCreateSamHoldingDocument() { // Arrange var now = DateTime.UtcNow; @@ -68,7 +74,10 @@ public void ToSilver_WithDefinitionRecordOnly_ShouldCreateSamHoldingDocument() }; // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands); + var result = await SamCommonLandMapper.ToSilver( + rawCommonLands, + ResolveCountry("country-id-1", "England"), + CancellationToken.None); // Assert result.Should().HaveCount(1); @@ -88,14 +97,15 @@ public void ToSilver_WithDefinitionRecordOnly_ShouldCreateSamHoldingDocument() holding.Location.Northing.Should().Be(569204); holding.Location.Address.Should().NotBeNull(); holding.Location.Address!.AddressLine.Should().Be("Land off Road"); - holding.Location.Address.AddressLocality.Should().Be("Village"); - holding.Location.Address.AddressStreet.Should().Be("District"); + holding.Location.Address.AddressStreet.Should().Be("Village"); + holding.Location.Address.AddressTown.Should().Be("District"); holding.Location.Address.AddressPostCode.Should().Be("AB12 3CD"); holding.Location.Address.CountryCode.Should().Be("England"); + holding.Location.Address.CountryIdentifier.Should().Be("country-id-1"); } [Fact] - public void ToSilver_WithDeletedRecord_ShouldMapDeletedStatus() + public async Task ToSilver_WithDeletedRecord_ShouldMapDeletedStatus() { // Arrange var rawCommonLands = new List @@ -112,7 +122,7 @@ public void ToSilver_WithDeletedRecord_ShouldMapDeletedStatus() }; // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands); + var result = await SamCommonLandMapper.ToSilver(rawCommonLands, NoopResolveCountry, CancellationToken.None); // Assert result.Should().HaveCount(1); @@ -121,7 +131,7 @@ public void ToSilver_WithDeletedRecord_ShouldMapDeletedStatus() } [Fact] - public void ToSilver_WithPlaceholderPremisesName_ShouldSetToNull() + public async Task ToSilver_WithPlaceholderPremisesName_ShouldSetToNull() { // Arrange var rawCommonLands = new List @@ -136,14 +146,14 @@ public void ToSilver_WithPlaceholderPremisesName_ShouldSetToNull() }; // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands); + var result = await SamCommonLandMapper.ToSilver(rawCommonLands, NoopResolveCountry, CancellationToken.None); // Assert result[0].LocationName.Should().BeNull(); } [Fact] - public void ToSilver_WithEmptyCommonCph_ShouldBeFiltered() + public async Task ToSilver_WithEmptyCommonCph_ShouldBeFiltered() { // Arrange var rawCommonLands = new List @@ -163,14 +173,14 @@ public void ToSilver_WithEmptyCommonCph_ShouldBeFiltered() }; // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands); + var result = await SamCommonLandMapper.ToSilver(rawCommonLands, NoopResolveCountry, CancellationToken.None); // Assert result.Should().BeEmpty(); } [Fact] - public void ToSilver_WithInvalidEastingNorthing_ShouldSetToNull() + public async Task ToSilver_WithInvalidEastingNorthing_ShouldSetToNull() { // Arrange var rawCommonLands = new List @@ -187,7 +197,7 @@ public void ToSilver_WithInvalidEastingNorthing_ShouldSetToNull() }; // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands); + var result = await SamCommonLandMapper.ToSilver(rawCommonLands, NoopResolveCountry, CancellationToken.None); // Assert result[0].Location!.Easting.Should().BeNull(); @@ -195,7 +205,7 @@ public void ToSilver_WithInvalidEastingNorthing_ShouldSetToNull() } [Fact] - public void ToSilver_WithFutureDate_ShouldNormaliseToNull() + public async Task ToSilver_WithFutureDate_ShouldNormaliseToNull() { // Arrange var rawCommonLands = new List @@ -216,7 +226,7 @@ public void ToSilver_WithFutureDate_ShouldNormaliseToNull() }; // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands); + var result = await SamCommonLandMapper.ToSilver(rawCommonLands, NoopResolveCountry, CancellationToken.None); // Assert result[1].AssociatedMainHoldings[0].StartDate.Should().BeNull(); @@ -376,7 +386,7 @@ public void ToAssociatedCommonLands_WithVariousDateFormats_ShouldNormaliseCorrec } [Fact] - public void ToSilver_ShouldMapBusinessUsageToSourceFacilitySubBusinessActivityCode() + public async Task ToSilver_ShouldMapBusinessUsageToSourceFacilitySubBusinessActivityCode() { // Arrange var rawCommonLands = new List @@ -392,7 +402,7 @@ public void ToSilver_ShouldMapBusinessUsageToSourceFacilitySubBusinessActivityCo }; // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands); + var result = await SamCommonLandMapper.ToSilver(rawCommonLands, NoopResolveCountry, CancellationToken.None); // Assert result.Should().HaveCount(1); @@ -400,7 +410,7 @@ public void ToSilver_ShouldMapBusinessUsageToSourceFacilitySubBusinessActivityCo } [Fact] - public void ToSilver_ShouldMapSiteTypeCodeToCL() + public async Task ToSilver_ShouldMapSiteTypeCodeToCL() { // Arrange var rawCommonLands = new List @@ -416,7 +426,7 @@ public void ToSilver_ShouldMapSiteTypeCodeToCL() }; // Act - var result = SamCommonLandMapper.ToSilver(rawCommonLands); + var result = await SamCommonLandMapper.ToSilver(rawCommonLands, NoopResolveCountry, CancellationToken.None); // Assert result.Should().HaveCount(1); 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 bb5d198c..73344442 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 @@ -787,6 +787,161 @@ public async Task WhenUpdatingSiteTypeSite_ItShouldUpdate() result.Type.Name.Should().Be("prem2name"); } + [Fact] + public void SelectAddressSource_WhenCommonLandAndSiteHoldingPresent_ShouldReturnCommonLand() + { + var siteHolding = new SamHoldingDocument + { + SourceFacilitySubBusinessActivityCode = "Sheep Farm", + HoldingStatus = "Active", + LastUpdatedDate = DateTime.UtcNow + }; + var commonLand = new SamHoldingDocument + { + SourceFacilitySubBusinessActivityCode = "Common Land", + HoldingStatus = "Active", + LastUpdatedDate = DateTime.UtcNow.AddDays(-1) + }; + + var result = SamHoldingMapper.SelectAddressSource([siteHolding, commonLand]); + + result.Should().BeSameAs(commonLand); + } + + [Fact] + public void SelectAddressSource_WhenNoCommonLandPresent_ShouldReturnRepresentative() + { + var siteHolding = new SamHoldingDocument + { + SourceFacilitySubBusinessActivityCode = "Sheep Farm", + HoldingStatus = "Active", + LastUpdatedDate = DateTime.UtcNow + }; + + var result = SamHoldingMapper.SelectAddressSource([siteHolding]); + + result.Should().BeSameAs(siteHolding); + } + + [Fact] + public void SelectAddressSource_WithMultipleCommonLands_ShouldReturnMostRecent() + { + var olderCommonLand = new SamHoldingDocument + { + SourceFacilitySubBusinessActivityCode = "Common Land", + LastUpdatedDate = DateTime.UtcNow.AddDays(-5) + }; + var newerCommonLand = new SamHoldingDocument + { + SourceFacilitySubBusinessActivityCode = "Common Land", + LastUpdatedDate = DateTime.UtcNow + }; + + var result = SamHoldingMapper.SelectAddressSource([olderCommonLand, newerCommonLand]); + + result.Should().BeSameAs(newerCommonLand); + } + + [Fact] + public async Task ToGold_WhenCommonLandAndSiteHoldingPresent_NewSite_ShouldUseCommonLandAddress() + { + var siteHolding = new SamHoldingDocument + { + SourceFacilitySubBusinessActivityCode = "Sheep Farm", + HoldingStatus = "Active", + LastUpdatedDate = DateTime.UtcNow, + Location = new Core.Documents.Silver.LocationDocument + { + IdentifierId = "site-loc", + Address = new Core.Documents.Silver.AddressDocument + { + IdentifierId = "site-addr", + AddressLine = "Site Road", + AddressPostCode = "S1 1AA", + CountryIdentifier = "en123" + } + } + }; + var commonLand = new SamHoldingDocument + { + SourceFacilitySubBusinessActivityCode = "Common Land", + HoldingStatus = "Active", + LastUpdatedDate = DateTime.UtcNow.AddDays(-1), + Location = new Core.Documents.Silver.LocationDocument + { + IdentifierId = "cl-loc", + Easting = 123456, + Northing = 654321, + Address = new Core.Documents.Silver.AddressDocument + { + IdentifierId = "cl-addr", + AddressLine = "Common Land Road", + AddressPostCode = "CL1 1AA", + CountryIdentifier = "fr123" + } + } + }; + + var result = await WhenIMapSilverSitesToGold([siteHolding, commonLand], null); + + result!.Location!.Address!.AddressLine1.Should().Be("Common Land Road"); + result.Location.Address.Postcode.Should().Be("CL1 1AA"); + result.Location.Address.Country!.Code.Should().Be("FR"); + result.Location.Easting.Should().Be(123456); + result.Location.Northing.Should().Be(654321); + result.Name.Should().Be(string.Empty); // name still comes from site representative + } + + [Fact] + public async Task ToGold_WhenCommonLandAndSiteHoldingPresent_ExistingSite_ShouldUseCommonLandAddress() + { + var siteHolding = new SamHoldingDocument + { + SourceFacilitySubBusinessActivityCode = "Sheep Farm", + HoldingStatus = "Active", + LastUpdatedDate = DateTime.UtcNow, + Location = new Core.Documents.Silver.LocationDocument + { + IdentifierId = "site-loc", + Address = new Core.Documents.Silver.AddressDocument + { + IdentifierId = "site-addr", + AddressLine = "Site Road", + AddressPostCode = "S1 1AA", + CountryIdentifier = "en123" + } + } + }; + var commonLand = new SamHoldingDocument + { + SourceFacilitySubBusinessActivityCode = "Common Land", + HoldingStatus = "Active", + LastUpdatedDate = DateTime.UtcNow.AddDays(-1), + Location = new Core.Documents.Silver.LocationDocument + { + IdentifierId = "cl-loc", + Easting = 123456, + Northing = 654321, + Address = new Core.Documents.Silver.AddressDocument + { + IdentifierId = "cl-addr", + AddressLine = "Common Land Road", + AddressPostCode = "CL1 1AA", + CountryIdentifier = "fr123" + } + } + }; + var existingSite = new SiteDocument { Id = GoldSiteId }; + + var result = await WhenIMapSilverSitesToGold([siteHolding, commonLand], existingSite); + + result!.Location!.Address!.AddressLine1.Should().Be("Common Land Road"); + result.Location.Address.Postcode.Should().Be("CL1 1AA"); + result.Location.Address.Country!.Code.Should().Be("FR"); + result.Location.Easting.Should().Be(123456); + result.Location.Northing.Should().Be(654321); + } + private static SiteGroupMarkRelationshipDocument CreateGroupMarkRelationshipDocument(string? id = "group-mark-id", string herdmark = "H1000001") { return new SiteGroupMarkRelationshipDocument()